@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
|
@@ -9599,6 +9599,89 @@ async function listAppointmentsOperation(params) {
|
|
|
9599
9599
|
);
|
|
9600
9600
|
}
|
|
9601
9601
|
|
|
9602
|
+
// src/data/search/engine/reference-predicate.ts
|
|
9603
|
+
function buildOpenHiResourceUrn(opts) {
|
|
9604
|
+
return `urn:ohi:${opts.tenantId}:${opts.workspaceId}:${opts.resourceType}:${opts.resourceId}`;
|
|
9605
|
+
}
|
|
9606
|
+
var REFERENCE_CONTAINMENT_SQL_FRAGMENT = "(resource @> :containmentRelative::jsonb OR resource @> :containmentUrn::jsonb)";
|
|
9607
|
+
function parseTypedReference(s) {
|
|
9608
|
+
const match = /^([A-Za-z][A-Za-z0-9_]*)\/([^\s/]+)$/.exec(s);
|
|
9609
|
+
if (!match) {
|
|
9610
|
+
return void 0;
|
|
9611
|
+
}
|
|
9612
|
+
return { resourceType: match[1], resourceId: match[2] };
|
|
9613
|
+
}
|
|
9614
|
+
function wrapReferenceInShape(shape, reference) {
|
|
9615
|
+
switch (shape.kind) {
|
|
9616
|
+
case "scalar":
|
|
9617
|
+
return { [shape.field]: { reference } };
|
|
9618
|
+
case "array-of-references":
|
|
9619
|
+
return { [shape.field]: [{ reference }] };
|
|
9620
|
+
case "array-of-objects":
|
|
9621
|
+
return { [shape.field]: [{ [shape.subfield]: { reference } }] };
|
|
9622
|
+
}
|
|
9623
|
+
}
|
|
9624
|
+
function buildReferenceContainmentPayload(params) {
|
|
9625
|
+
const parsed = parseTypedReference(params.reference);
|
|
9626
|
+
if (!parsed) {
|
|
9627
|
+
throw new Error(
|
|
9628
|
+
`Reference "${params.reference}" is not a valid typed reference (<ResourceType>/<id>).`
|
|
9629
|
+
);
|
|
9630
|
+
}
|
|
9631
|
+
const urn = buildOpenHiResourceUrn({
|
|
9632
|
+
tenantId: params.tenantId,
|
|
9633
|
+
workspaceId: params.workspaceId,
|
|
9634
|
+
resourceType: parsed.resourceType,
|
|
9635
|
+
resourceId: parsed.resourceId
|
|
9636
|
+
});
|
|
9637
|
+
return {
|
|
9638
|
+
containmentRelative: JSON.stringify(
|
|
9639
|
+
wrapReferenceInShape(params.shape, params.reference)
|
|
9640
|
+
),
|
|
9641
|
+
containmentUrn: JSON.stringify(wrapReferenceInShape(params.shape, urn))
|
|
9642
|
+
};
|
|
9643
|
+
}
|
|
9644
|
+
function jsonbPathToReferenceShape(jsonbPath) {
|
|
9645
|
+
const scalar = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
|
|
9646
|
+
if (scalar) {
|
|
9647
|
+
return { kind: "scalar", field: scalar[1] };
|
|
9648
|
+
}
|
|
9649
|
+
const arrayOfRefs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
|
|
9650
|
+
if (arrayOfRefs) {
|
|
9651
|
+
return { kind: "array-of-references", field: arrayOfRefs[1] };
|
|
9652
|
+
}
|
|
9653
|
+
const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
|
|
9654
|
+
jsonbPath
|
|
9655
|
+
);
|
|
9656
|
+
if (arrayOfObjs) {
|
|
9657
|
+
return {
|
|
9658
|
+
kind: "array-of-objects",
|
|
9659
|
+
field: arrayOfObjs[1],
|
|
9660
|
+
subfield: arrayOfObjs[2]
|
|
9661
|
+
};
|
|
9662
|
+
}
|
|
9663
|
+
throw new Error(
|
|
9664
|
+
`Reference predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
|
|
9665
|
+
);
|
|
9666
|
+
}
|
|
9667
|
+
function emitReferencePredicate(opts) {
|
|
9668
|
+
const shape = jsonbPathToReferenceShape(opts.jsonbPath);
|
|
9669
|
+
const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
|
|
9670
|
+
shape,
|
|
9671
|
+
reference: opts.rawValue,
|
|
9672
|
+
tenantId: opts.context.tenantId,
|
|
9673
|
+
workspaceId: opts.context.workspaceId
|
|
9674
|
+
});
|
|
9675
|
+
const relName = `${opts.paramName}R`;
|
|
9676
|
+
const urnName = `${opts.paramName}U`;
|
|
9677
|
+
const sql = `(resource @> :${relName}::jsonb OR resource @> :${urnName}::jsonb)`;
|
|
9678
|
+
const params = [
|
|
9679
|
+
{ name: relName, value: containmentRelative },
|
|
9680
|
+
{ name: urnName, value: containmentUrn }
|
|
9681
|
+
];
|
|
9682
|
+
return { sql, params };
|
|
9683
|
+
}
|
|
9684
|
+
|
|
9602
9685
|
// src/data/postgres/data-api-postgres-query-runner.ts
|
|
9603
9686
|
var import_client_rds_data = require("@aws-sdk/client-rds-data");
|
|
9604
9687
|
|
|
@@ -9785,13 +9868,17 @@ function parseDateSearchValue(raw) {
|
|
|
9785
9868
|
return { prefix: "eq", value: raw };
|
|
9786
9869
|
}
|
|
9787
9870
|
function flatJsonbExtract(jsonbPath) {
|
|
9788
|
-
const
|
|
9789
|
-
if (
|
|
9790
|
-
|
|
9791
|
-
`Generic date predicate requires a flat top-level JSONPath like "$.fieldName"; received "${jsonbPath}".`
|
|
9792
|
-
);
|
|
9871
|
+
const topLevel = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
|
|
9872
|
+
if (topLevel) {
|
|
9873
|
+
return `resource->>'${topLevel[1]}'`;
|
|
9793
9874
|
}
|
|
9794
|
-
|
|
9875
|
+
const nested = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
|
|
9876
|
+
if (nested) {
|
|
9877
|
+
return `resource->'${nested[1]}'->>'${nested[2]}'`;
|
|
9878
|
+
}
|
|
9879
|
+
throw new Error(
|
|
9880
|
+
`Generic date predicate requires a scalar JSONPath like "$.fieldName" or "$.field.subfield"; received "${jsonbPath}".`
|
|
9881
|
+
);
|
|
9795
9882
|
}
|
|
9796
9883
|
function emitDatePredicate(opts) {
|
|
9797
9884
|
const { jsonbPath, rawValue, paramName } = opts;
|
|
@@ -9827,129 +9914,95 @@ function emitFieldMissingPredicate(opts) {
|
|
|
9827
9914
|
const sql = opts.missing ? `${extract} IS NULL` : `${extract} IS NOT NULL`;
|
|
9828
9915
|
return { sql, params: [] };
|
|
9829
9916
|
}
|
|
9830
|
-
|
|
9831
|
-
|
|
9832
|
-
|
|
9917
|
+
|
|
9918
|
+
// src/data/search/engine/string-predicate.ts
|
|
9919
|
+
var STRING_MODIFIERS = ["exact", "contains"];
|
|
9920
|
+
function isStringModifier(s) {
|
|
9921
|
+
return STRING_MODIFIERS.includes(s);
|
|
9833
9922
|
}
|
|
9834
|
-
function
|
|
9835
|
-
|
|
9836
|
-
|
|
9837
|
-
|
|
9838
|
-
|
|
9839
|
-
|
|
9840
|
-
|
|
9841
|
-
|
|
9842
|
-
|
|
9843
|
-
|
|
9844
|
-
|
|
9845
|
-
return `(${startExtract} IS NULL OR ${startExtract} <= :${paramName})`;
|
|
9846
|
-
case "sa":
|
|
9847
|
-
return `(${startExtract} IS NOT NULL AND ${startExtract} > :${paramName})`;
|
|
9848
|
-
case "eb":
|
|
9849
|
-
return `(${endExtract} IS NOT NULL AND ${endExtract} < :${paramName})`;
|
|
9850
|
-
}
|
|
9851
|
-
}
|
|
9852
|
-
function buildIntervalDateSearchPredicateSql(constraints, opts) {
|
|
9853
|
-
if (constraints.length === 0) {
|
|
9854
|
-
return [];
|
|
9855
|
-
}
|
|
9856
|
-
const paramPrefix = opts.paramNamePrefix ?? DEFAULT_INTERVAL_PARAM_PREFIX;
|
|
9857
|
-
const startExtract = flatJsonbExtract(opts.startPath);
|
|
9858
|
-
const endExtract = flatJsonbExtract(opts.endPath);
|
|
9859
|
-
const fragments = constraints.map(
|
|
9860
|
-
(c, i) => buildIntervalSingleSql(
|
|
9861
|
-
c.prefix,
|
|
9862
|
-
startExtract,
|
|
9863
|
-
endExtract,
|
|
9864
|
-
intervalConstraintParamName(paramPrefix, i)
|
|
9865
|
-
)
|
|
9923
|
+
function jsonbPathToStringShape(jsonbPath) {
|
|
9924
|
+
const scalar = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
|
|
9925
|
+
if (scalar) {
|
|
9926
|
+
return { kind: "scalar", field: scalar[1] };
|
|
9927
|
+
}
|
|
9928
|
+
const arrayOfScalars = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
|
|
9929
|
+
if (arrayOfScalars) {
|
|
9930
|
+
return { kind: "array-of-scalars", field: arrayOfScalars[1] };
|
|
9931
|
+
}
|
|
9932
|
+
const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
|
|
9933
|
+
jsonbPath
|
|
9866
9934
|
);
|
|
9867
|
-
|
|
9868
|
-
|
|
9869
|
-
|
|
9870
|
-
|
|
9871
|
-
|
|
9872
|
-
|
|
9873
|
-
|
|
9874
|
-
|
|
9875
|
-
|
|
9876
|
-
}
|
|
9877
|
-
var APPOINTMENT_DATE_SEARCH_PREFIXES = [
|
|
9878
|
-
"gt",
|
|
9879
|
-
"lt",
|
|
9880
|
-
"ge",
|
|
9881
|
-
"le",
|
|
9882
|
-
"sa",
|
|
9883
|
-
"eb"
|
|
9884
|
-
];
|
|
9885
|
-
function isAppointmentDateSearchPrefix(s) {
|
|
9886
|
-
return APPOINTMENT_DATE_SEARCH_PREFIXES.includes(
|
|
9887
|
-
s
|
|
9935
|
+
if (arrayOfObjs) {
|
|
9936
|
+
return {
|
|
9937
|
+
kind: "array-of-objects",
|
|
9938
|
+
field: arrayOfObjs[1],
|
|
9939
|
+
subfield: arrayOfObjs[2]
|
|
9940
|
+
};
|
|
9941
|
+
}
|
|
9942
|
+
throw new Error(
|
|
9943
|
+
`String predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
|
|
9888
9944
|
);
|
|
9889
9945
|
}
|
|
9890
|
-
function
|
|
9891
|
-
return
|
|
9892
|
-
startPath: "$.start",
|
|
9893
|
-
endPath: "$.end",
|
|
9894
|
-
paramNamePrefix: "apptDateConstraint"
|
|
9895
|
-
});
|
|
9896
|
-
}
|
|
9897
|
-
function buildAppointmentDateSearchPredicateParams(constraints) {
|
|
9898
|
-
return buildIntervalDateSearchPredicateParams(constraints, {
|
|
9899
|
-
paramNamePrefix: "apptDateConstraint"
|
|
9900
|
-
});
|
|
9901
|
-
}
|
|
9902
|
-
|
|
9903
|
-
// src/data/search/engine/reference-predicate.ts
|
|
9904
|
-
function buildOpenHiResourceUrn(opts) {
|
|
9905
|
-
return `urn:ohi:${opts.tenantId}:${opts.workspaceId}:${opts.resourceType}:${opts.resourceId}`;
|
|
9946
|
+
function escapeLikePattern(value) {
|
|
9947
|
+
return value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
9906
9948
|
}
|
|
9907
|
-
|
|
9908
|
-
|
|
9909
|
-
|
|
9910
|
-
|
|
9911
|
-
|
|
9949
|
+
function buildLikePattern(value, modifier) {
|
|
9950
|
+
const escaped = escapeLikePattern(value);
|
|
9951
|
+
switch (modifier) {
|
|
9952
|
+
case "exact":
|
|
9953
|
+
return escaped;
|
|
9954
|
+
case "contains":
|
|
9955
|
+
return `%${escaped}%`;
|
|
9956
|
+
case void 0:
|
|
9957
|
+
return `${escaped}%`;
|
|
9912
9958
|
}
|
|
9913
|
-
return { resourceType: match[1], resourceId: match[2] };
|
|
9914
9959
|
}
|
|
9915
|
-
function
|
|
9960
|
+
function buildIlikeExtractSql(shape, paramName) {
|
|
9916
9961
|
switch (shape.kind) {
|
|
9917
9962
|
case "scalar":
|
|
9918
|
-
return {
|
|
9919
|
-
case "array-of-
|
|
9920
|
-
return
|
|
9963
|
+
return `resource->>'${shape.field}' ILIKE :${paramName}`;
|
|
9964
|
+
case "array-of-scalars":
|
|
9965
|
+
return [
|
|
9966
|
+
"EXISTS (SELECT 1 FROM",
|
|
9967
|
+
`jsonb_array_elements_text(resource->'${shape.field}') AS s_elem(text_val)`,
|
|
9968
|
+
`WHERE s_elem.text_val ILIKE :${paramName})`
|
|
9969
|
+
].join(" ");
|
|
9921
9970
|
case "array-of-objects":
|
|
9922
|
-
return
|
|
9971
|
+
return [
|
|
9972
|
+
"EXISTS (SELECT 1 FROM",
|
|
9973
|
+
`jsonb_array_elements(resource->'${shape.field}') AS s_obj(obj)`,
|
|
9974
|
+
`WHERE s_obj.obj->>'${shape.subfield}' ILIKE :${paramName})`
|
|
9975
|
+
].join(" ");
|
|
9923
9976
|
}
|
|
9924
9977
|
}
|
|
9925
|
-
function
|
|
9926
|
-
const
|
|
9927
|
-
|
|
9978
|
+
function emitStringPredicate(opts) {
|
|
9979
|
+
const shape = jsonbPathToStringShape(opts.jsonbPath);
|
|
9980
|
+
const modifier = opts.modifier === void 0 ? void 0 : checkModifier(opts.modifier);
|
|
9981
|
+
const sql = buildIlikeExtractSql(shape, opts.paramName);
|
|
9982
|
+
const pattern = buildLikePattern(opts.rawValue, modifier);
|
|
9983
|
+
const params = [
|
|
9984
|
+
{ name: opts.paramName, value: pattern }
|
|
9985
|
+
];
|
|
9986
|
+
return { sql, params };
|
|
9987
|
+
}
|
|
9988
|
+
function checkModifier(modifier) {
|
|
9989
|
+
if (!isStringModifier(modifier)) {
|
|
9928
9990
|
throw new Error(
|
|
9929
|
-
`
|
|
9991
|
+
`String predicate does not support modifier ":${modifier}". Supported: ${STRING_MODIFIERS.map((m) => `:${m}`).join(", ")}.`
|
|
9930
9992
|
);
|
|
9931
9993
|
}
|
|
9932
|
-
|
|
9933
|
-
tenantId: params.tenantId,
|
|
9934
|
-
workspaceId: params.workspaceId,
|
|
9935
|
-
resourceType: parsed.resourceType,
|
|
9936
|
-
resourceId: parsed.resourceId
|
|
9937
|
-
});
|
|
9938
|
-
return {
|
|
9939
|
-
containmentRelative: JSON.stringify(
|
|
9940
|
-
wrapReferenceInShape(params.shape, params.reference)
|
|
9941
|
-
),
|
|
9942
|
-
containmentUrn: JSON.stringify(wrapReferenceInShape(params.shape, urn))
|
|
9943
|
-
};
|
|
9994
|
+
return modifier;
|
|
9944
9995
|
}
|
|
9945
|
-
|
|
9996
|
+
|
|
9997
|
+
// src/data/search/engine/token-predicate.ts
|
|
9998
|
+
function jsonbPathToTokenShape(jsonbPath) {
|
|
9946
9999
|
const scalar = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
|
|
9947
10000
|
if (scalar) {
|
|
9948
10001
|
return { kind: "scalar", field: scalar[1] };
|
|
9949
10002
|
}
|
|
9950
|
-
const
|
|
9951
|
-
if (
|
|
9952
|
-
return { kind: "array-of-
|
|
10003
|
+
const arrayOfScalars = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
|
|
10004
|
+
if (arrayOfScalars) {
|
|
10005
|
+
return { kind: "array-of-scalars", field: arrayOfScalars[1] };
|
|
9953
10006
|
}
|
|
9954
10007
|
const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
|
|
9955
10008
|
jsonbPath
|
|
@@ -9962,240 +10015,408 @@ function jsonbPathToReferenceShape(jsonbPath) {
|
|
|
9962
10015
|
};
|
|
9963
10016
|
}
|
|
9964
10017
|
throw new Error(
|
|
9965
|
-
`
|
|
10018
|
+
`Token predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
|
|
9966
10019
|
);
|
|
9967
10020
|
}
|
|
9968
|
-
function
|
|
9969
|
-
const
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
}
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
10021
|
+
function parseTokenValue(raw) {
|
|
10022
|
+
const idx = raw.indexOf("|");
|
|
10023
|
+
if (idx === -1) {
|
|
10024
|
+
return { code: raw };
|
|
10025
|
+
}
|
|
10026
|
+
const system = raw.slice(0, idx);
|
|
10027
|
+
const code = raw.slice(idx + 1);
|
|
10028
|
+
return system.length > 0 ? { system, code } : { code };
|
|
10029
|
+
}
|
|
10030
|
+
function wrapTokenInShape(shape, value) {
|
|
10031
|
+
switch (shape.kind) {
|
|
10032
|
+
case "scalar":
|
|
10033
|
+
return { [shape.field]: value };
|
|
10034
|
+
case "array-of-scalars":
|
|
10035
|
+
return { [shape.field]: [value] };
|
|
10036
|
+
case "array-of-objects":
|
|
10037
|
+
return { [shape.field]: [{ [shape.subfield]: value }] };
|
|
10038
|
+
}
|
|
10039
|
+
}
|
|
10040
|
+
function emitTokenPredicate(opts) {
|
|
10041
|
+
const shape = jsonbPathToTokenShape(opts.jsonbPath);
|
|
10042
|
+
const { code } = parseTokenValue(opts.rawValue);
|
|
10043
|
+
const payload = wrapTokenInShape(shape, code);
|
|
10044
|
+
const sql = `resource @> :${opts.paramName}::jsonb`;
|
|
9979
10045
|
const params = [
|
|
9980
|
-
{ name:
|
|
9981
|
-
{ name: urnName, value: containmentUrn }
|
|
10046
|
+
{ name: opts.paramName, value: JSON.stringify(payload) }
|
|
9982
10047
|
];
|
|
9983
10048
|
return { sql, params };
|
|
9984
10049
|
}
|
|
9985
10050
|
|
|
9986
|
-
// src/data/
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
9991
|
-
);
|
|
9992
|
-
const lines = [
|
|
9993
|
-
"SELECT resource_id AS id, resource",
|
|
9994
|
-
"FROM resources",
|
|
9995
|
-
"WHERE tenant_id = :tenantId",
|
|
9996
|
-
" AND workspace_id = :workspaceId",
|
|
9997
|
-
" AND resource_type = 'Appointment'",
|
|
9998
|
-
" AND deleted_at IS NULL",
|
|
9999
|
-
` AND ${REFERENCE_CONTAINMENT_SQL_FRAGMENT}`
|
|
10000
|
-
];
|
|
10001
|
-
for (const fragment of datePredicates) {
|
|
10002
|
-
lines.push(` AND ${fragment}`);
|
|
10051
|
+
// src/data/search/engine/combinator.ts
|
|
10052
|
+
function parseQueryKey(key) {
|
|
10053
|
+
const idx = key.indexOf(":");
|
|
10054
|
+
if (idx === -1) {
|
|
10055
|
+
return { code: key, modifier: void 0 };
|
|
10003
10056
|
}
|
|
10004
|
-
|
|
10005
|
-
lines.push("LIMIT :limit;");
|
|
10006
|
-
return lines.join("\n");
|
|
10057
|
+
return { code: key.slice(0, idx), modifier: key.slice(idx + 1) };
|
|
10007
10058
|
}
|
|
10008
|
-
|
|
10009
|
-
const
|
|
10010
|
-
const
|
|
10011
|
-
const
|
|
10012
|
-
|
|
10013
|
-
|
|
10014
|
-
const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
|
|
10015
|
-
shape: {
|
|
10016
|
-
kind: "array-of-objects",
|
|
10017
|
-
field: "participant",
|
|
10018
|
-
subfield: "actor"
|
|
10019
|
-
},
|
|
10020
|
-
reference: actorReference,
|
|
10021
|
-
tenantId,
|
|
10022
|
-
workspaceId
|
|
10023
|
-
});
|
|
10024
|
-
const sql = buildSearchAppointmentsByActorSql({ dateConstraints });
|
|
10025
|
-
const queryParams = [
|
|
10026
|
-
{ name: "tenantId", value: tenantId },
|
|
10027
|
-
{ name: "workspaceId", value: workspaceId },
|
|
10028
|
-
{ name: "containmentRelative", value: containmentRelative },
|
|
10029
|
-
{ name: "containmentUrn", value: containmentUrn },
|
|
10030
|
-
{ name: "limit", value: limit },
|
|
10031
|
-
...buildAppointmentDateSearchPredicateParams(dateConstraints)
|
|
10032
|
-
];
|
|
10033
|
-
const rows = await runner.query(sql, queryParams);
|
|
10034
|
-
const entries = rows.map((row) => ({
|
|
10035
|
-
id: row.id,
|
|
10036
|
-
resource: {
|
|
10037
|
-
...row.resource,
|
|
10038
|
-
id: row.id
|
|
10059
|
+
function flattenQueryValues(raw) {
|
|
10060
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
10061
|
+
const out = [];
|
|
10062
|
+
for (const v of list) {
|
|
10063
|
+
for (const piece of v.split(",")) {
|
|
10064
|
+
out.push(piece);
|
|
10039
10065
|
}
|
|
10040
|
-
}
|
|
10041
|
-
return
|
|
10066
|
+
}
|
|
10067
|
+
return out;
|
|
10042
10068
|
}
|
|
10043
|
-
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10069
|
+
function parseQueryEntries(query) {
|
|
10070
|
+
const entries = [];
|
|
10071
|
+
for (const [rawKey, rawValue] of Object.entries(query)) {
|
|
10072
|
+
if (rawValue === void 0) continue;
|
|
10073
|
+
const { code, modifier } = parseQueryKey(rawKey);
|
|
10074
|
+
entries.push({
|
|
10075
|
+
code,
|
|
10076
|
+
modifier,
|
|
10077
|
+
values: flattenQueryValues(rawValue)
|
|
10078
|
+
});
|
|
10079
|
+
}
|
|
10080
|
+
return entries;
|
|
10081
|
+
}
|
|
10082
|
+
function findParam(params, code) {
|
|
10083
|
+
return params.find((p) => p.code === code);
|
|
10084
|
+
}
|
|
10085
|
+
function checkModifierAllowed(modifier, param) {
|
|
10086
|
+
const universal = modifier === "missing" || modifier === "not";
|
|
10087
|
+
const stringNative = param.type === "string" && (modifier === "exact" || modifier === "contains");
|
|
10088
|
+
if (universal || stringNative) {
|
|
10089
|
+
if (param.modifiers && !param.modifiers.includes(modifier)) {
|
|
10090
|
+
throw new Error(
|
|
10091
|
+
`Modifier ":${modifier}" is not in the allow-list for param "${param.code}".`
|
|
10092
|
+
);
|
|
10093
|
+
}
|
|
10094
|
+
return;
|
|
10095
|
+
}
|
|
10096
|
+
throw new Error(
|
|
10097
|
+
`Modifier ":${modifier}" is not recognized for param "${param.code}" (type "${param.type}").`
|
|
10049
10098
|
);
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
lines.push(` AND ${fragment}`);
|
|
10099
|
+
}
|
|
10100
|
+
function emitOne(opts) {
|
|
10101
|
+
const { param, modifier, rawValue, paramName, context } = opts;
|
|
10102
|
+
if (modifier === "missing") {
|
|
10103
|
+
const missing = parseMissingValue(rawValue);
|
|
10104
|
+
return emitFieldMissingPredicate({
|
|
10105
|
+
jsonbPath: param.jsonbPath,
|
|
10106
|
+
missing
|
|
10107
|
+
});
|
|
10060
10108
|
}
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10109
|
+
const negate = modifier === "not";
|
|
10110
|
+
const effectiveModifier = negate ? void 0 : modifier;
|
|
10111
|
+
const inner = emitForType({
|
|
10112
|
+
paramType: param.type,
|
|
10113
|
+
jsonbPath: param.jsonbPath,
|
|
10114
|
+
rawValue,
|
|
10115
|
+
paramName,
|
|
10116
|
+
modifier: effectiveModifier,
|
|
10117
|
+
context
|
|
10118
|
+
});
|
|
10119
|
+
if (!negate) {
|
|
10120
|
+
return inner;
|
|
10121
|
+
}
|
|
10122
|
+
return { sql: `NOT (${inner.sql})`, params: inner.params };
|
|
10064
10123
|
}
|
|
10065
|
-
|
|
10066
|
-
|
|
10067
|
-
if (
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
|
|
10124
|
+
function parseMissingValue(raw) {
|
|
10125
|
+
if (raw === "true") return true;
|
|
10126
|
+
if (raw === "false") return false;
|
|
10127
|
+
throw new Error(
|
|
10128
|
+
`:missing requires a value of "true" or "false"; received "${raw}".`
|
|
10129
|
+
);
|
|
10130
|
+
}
|
|
10131
|
+
function emitForType(opts) {
|
|
10132
|
+
switch (opts.paramType) {
|
|
10133
|
+
case "token":
|
|
10134
|
+
return emitTokenPredicate({
|
|
10135
|
+
jsonbPath: opts.jsonbPath,
|
|
10136
|
+
rawValue: opts.rawValue,
|
|
10137
|
+
paramName: opts.paramName,
|
|
10138
|
+
context: opts.context
|
|
10139
|
+
});
|
|
10140
|
+
case "date":
|
|
10141
|
+
return emitDatePredicate({
|
|
10142
|
+
jsonbPath: opts.jsonbPath,
|
|
10143
|
+
rawValue: opts.rawValue,
|
|
10144
|
+
paramName: opts.paramName,
|
|
10145
|
+
context: opts.context
|
|
10146
|
+
});
|
|
10147
|
+
case "reference":
|
|
10148
|
+
return emitReferencePredicate({
|
|
10149
|
+
jsonbPath: opts.jsonbPath,
|
|
10150
|
+
rawValue: opts.rawValue,
|
|
10151
|
+
paramName: opts.paramName,
|
|
10152
|
+
context: opts.context
|
|
10153
|
+
});
|
|
10154
|
+
case "string":
|
|
10155
|
+
return emitStringPredicate({
|
|
10156
|
+
jsonbPath: opts.jsonbPath,
|
|
10157
|
+
rawValue: opts.rawValue,
|
|
10158
|
+
paramName: opts.paramName,
|
|
10159
|
+
modifier: opts.modifier,
|
|
10160
|
+
context: opts.context
|
|
10161
|
+
});
|
|
10071
10162
|
}
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
const
|
|
10075
|
-
const
|
|
10076
|
-
const
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
const rows = await runner.query(sql, queryParams);
|
|
10083
|
-
const entries = rows.map((row) => ({
|
|
10084
|
-
id: row.id,
|
|
10085
|
-
resource: {
|
|
10086
|
-
...row.resource,
|
|
10087
|
-
id: row.id
|
|
10163
|
+
}
|
|
10164
|
+
function combineSearchPredicates(opts) {
|
|
10165
|
+
const entries = parseQueryEntries(opts.query);
|
|
10166
|
+
const groupSqls = [];
|
|
10167
|
+
const allParams = [];
|
|
10168
|
+
let paramIdx = 0;
|
|
10169
|
+
for (const entry of entries) {
|
|
10170
|
+
const registered = findParam(opts.registeredParams, entry.code);
|
|
10171
|
+
if (!registered) {
|
|
10172
|
+
continue;
|
|
10088
10173
|
}
|
|
10089
|
-
|
|
10090
|
-
|
|
10174
|
+
if (entry.modifier !== void 0) {
|
|
10175
|
+
checkModifierAllowed(entry.modifier, registered);
|
|
10176
|
+
}
|
|
10177
|
+
if (entry.values.length === 0) {
|
|
10178
|
+
continue;
|
|
10179
|
+
}
|
|
10180
|
+
const fragmentSqls = [];
|
|
10181
|
+
let valueIdx = 0;
|
|
10182
|
+
for (const rawValue of entry.values) {
|
|
10183
|
+
const paramName = `p${paramIdx}v${valueIdx}`;
|
|
10184
|
+
const frag = emitOne({
|
|
10185
|
+
param: registered,
|
|
10186
|
+
modifier: entry.modifier,
|
|
10187
|
+
rawValue,
|
|
10188
|
+
paramName,
|
|
10189
|
+
context: opts.context
|
|
10190
|
+
});
|
|
10191
|
+
fragmentSqls.push(frag.sql);
|
|
10192
|
+
for (const p of frag.params) {
|
|
10193
|
+
allParams.push(p);
|
|
10194
|
+
}
|
|
10195
|
+
valueIdx++;
|
|
10196
|
+
}
|
|
10197
|
+
paramIdx++;
|
|
10198
|
+
if (fragmentSqls.length === 1) {
|
|
10199
|
+
groupSqls.push(fragmentSqls[0]);
|
|
10200
|
+
} else {
|
|
10201
|
+
groupSqls.push(`(${fragmentSqls.join(" OR ")})`);
|
|
10202
|
+
}
|
|
10203
|
+
}
|
|
10204
|
+
if (groupSqls.length === 0) {
|
|
10205
|
+
return { sql: "", params: [] };
|
|
10206
|
+
}
|
|
10207
|
+
return { sql: groupSqls.join(" AND "), params: allParams };
|
|
10091
10208
|
}
|
|
10092
10209
|
|
|
10093
|
-
// src/data/operations/
|
|
10094
|
-
var
|
|
10095
|
-
function
|
|
10096
|
-
const datePredicates = buildAppointmentDateSearchPredicateSql(
|
|
10097
|
-
opts?.dateConstraints ?? []
|
|
10098
|
-
);
|
|
10210
|
+
// src/data/search/operations/generic-search-operation.ts
|
|
10211
|
+
var DEFAULT_LIMIT = 100;
|
|
10212
|
+
function buildGenericSearchSql(opts) {
|
|
10099
10213
|
const lines = [
|
|
10100
10214
|
"SELECT resource_id AS id, resource",
|
|
10101
10215
|
"FROM resources",
|
|
10102
10216
|
"WHERE tenant_id = :tenantId",
|
|
10103
10217
|
" AND workspace_id = :workspaceId",
|
|
10104
|
-
" AND resource_type =
|
|
10105
|
-
" AND deleted_at IS NULL"
|
|
10106
|
-
" AND (resource @> :containmentRelative::jsonb",
|
|
10107
|
-
" OR resource @> :containmentUrn::jsonb)"
|
|
10218
|
+
" AND resource_type = :resourceType",
|
|
10219
|
+
" AND deleted_at IS NULL"
|
|
10108
10220
|
];
|
|
10109
|
-
|
|
10110
|
-
lines.push(` AND ${
|
|
10221
|
+
if (opts.combinedSql.length > 0) {
|
|
10222
|
+
lines.push(` AND (${opts.combinedSql})`);
|
|
10111
10223
|
}
|
|
10112
10224
|
lines.push("ORDER BY last_updated DESC");
|
|
10113
10225
|
lines.push("LIMIT :limit;");
|
|
10114
10226
|
return lines.join("\n");
|
|
10115
10227
|
}
|
|
10116
|
-
async function
|
|
10117
|
-
const {
|
|
10118
|
-
const dateConstraints = params.dateConstraints ?? [];
|
|
10119
|
-
const { tenantId, workspaceId } = context;
|
|
10228
|
+
async function genericSearchOperation(params) {
|
|
10229
|
+
const { resourceType, tenantId, workspaceId, query, resolver } = params;
|
|
10120
10230
|
const runner = params.runner ?? getDefaultPostgresQueryRunner();
|
|
10121
|
-
const limit = params.limit ??
|
|
10122
|
-
const
|
|
10123
|
-
|
|
10124
|
-
|
|
10125
|
-
|
|
10126
|
-
|
|
10127
|
-
|
|
10128
|
-
actor: {
|
|
10129
|
-
reference: buildOpenHiResourceUrn({
|
|
10130
|
-
tenantId,
|
|
10131
|
-
workspaceId,
|
|
10132
|
-
resourceType: "Patient",
|
|
10133
|
-
resourceId: patientId
|
|
10134
|
-
})
|
|
10135
|
-
}
|
|
10136
|
-
}
|
|
10137
|
-
]
|
|
10231
|
+
const limit = params.limit ?? DEFAULT_LIMIT;
|
|
10232
|
+
const registeredParams = resolver(resourceType, tenantId);
|
|
10233
|
+
const context = { tenantId, workspaceId, resourceType };
|
|
10234
|
+
const combined = combineSearchPredicates({
|
|
10235
|
+
query,
|
|
10236
|
+
registeredParams,
|
|
10237
|
+
context
|
|
10138
10238
|
});
|
|
10139
|
-
const sql =
|
|
10239
|
+
const sql = buildGenericSearchSql({ combinedSql: combined.sql });
|
|
10140
10240
|
const queryParams = [
|
|
10141
10241
|
{ name: "tenantId", value: tenantId },
|
|
10142
10242
|
{ name: "workspaceId", value: workspaceId },
|
|
10143
|
-
{ name: "
|
|
10144
|
-
{ name: "containmentUrn", value: containmentUrn },
|
|
10243
|
+
{ name: "resourceType", value: resourceType },
|
|
10145
10244
|
{ name: "limit", value: limit },
|
|
10146
|
-
...
|
|
10245
|
+
...combined.params
|
|
10147
10246
|
];
|
|
10148
10247
|
const rows = await runner.query(sql, queryParams);
|
|
10149
10248
|
const entries = rows.map((row) => ({
|
|
10150
10249
|
id: row.id,
|
|
10151
|
-
resource: {
|
|
10152
|
-
...row.resource,
|
|
10153
|
-
id: row.id
|
|
10154
|
-
}
|
|
10250
|
+
resource: { ...row.resource, id: row.id }
|
|
10155
10251
|
}));
|
|
10156
10252
|
return { entries, total: entries.length };
|
|
10157
10253
|
}
|
|
10158
10254
|
|
|
10159
|
-
// src/data/
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
}
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
|
|
10183
|
-
|
|
10184
|
-
|
|
10185
|
-
|
|
10186
|
-
|
|
10187
|
-
|
|
10188
|
-
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
|
|
10193
|
-
|
|
10194
|
-
|
|
10195
|
-
|
|
10196
|
-
|
|
10255
|
+
// src/data/search/registry/appointment-search-parameters.ts
|
|
10256
|
+
var APPOINTMENT_SEARCH_PARAMETERS = [
|
|
10257
|
+
{ code: "status", type: "token", jsonbPath: "$.status" },
|
|
10258
|
+
{ code: "date", type: "date", jsonbPath: "$.start" },
|
|
10259
|
+
{
|
|
10260
|
+
code: "patient",
|
|
10261
|
+
type: "reference",
|
|
10262
|
+
jsonbPath: "$.participant[*].actor"
|
|
10263
|
+
},
|
|
10264
|
+
{
|
|
10265
|
+
code: "actor",
|
|
10266
|
+
type: "reference",
|
|
10267
|
+
jsonbPath: "$.participant[*].actor"
|
|
10268
|
+
},
|
|
10269
|
+
{
|
|
10270
|
+
code: "practitioner",
|
|
10271
|
+
type: "reference",
|
|
10272
|
+
jsonbPath: "$.participant[*].actor"
|
|
10273
|
+
},
|
|
10274
|
+
{ code: "service-type", type: "token", jsonbPath: "$.serviceType[*]" },
|
|
10275
|
+
{
|
|
10276
|
+
code: "service-category",
|
|
10277
|
+
type: "token",
|
|
10278
|
+
jsonbPath: "$.serviceCategory[*]"
|
|
10279
|
+
},
|
|
10280
|
+
{ code: "specialty", type: "token", jsonbPath: "$.specialty[*]" },
|
|
10281
|
+
{
|
|
10282
|
+
code: "appointment-type",
|
|
10283
|
+
type: "token",
|
|
10284
|
+
jsonbPath: "$.appointmentType"
|
|
10285
|
+
},
|
|
10286
|
+
{ code: "slot", type: "reference", jsonbPath: "$.slot[*]" }
|
|
10287
|
+
];
|
|
10288
|
+
|
|
10289
|
+
// src/data/search/registry/encounter-search-parameters.ts
|
|
10290
|
+
var ENCOUNTER_SEARCH_PARAMETERS = [
|
|
10291
|
+
{ code: "status", type: "token", jsonbPath: "$.status" },
|
|
10292
|
+
{ code: "class", type: "token", jsonbPath: "$.class" },
|
|
10293
|
+
{ code: "type", type: "token", jsonbPath: "$.type[*]" },
|
|
10294
|
+
{ code: "subject", type: "reference", jsonbPath: "$.subject" },
|
|
10295
|
+
{ code: "patient", type: "reference", jsonbPath: "$.subject" },
|
|
10296
|
+
{
|
|
10297
|
+
code: "participant",
|
|
10298
|
+
type: "reference",
|
|
10299
|
+
jsonbPath: "$.participant[*].individual"
|
|
10300
|
+
},
|
|
10301
|
+
{ code: "date", type: "date", jsonbPath: "$.period.start" },
|
|
10302
|
+
{
|
|
10303
|
+
code: "service-provider",
|
|
10304
|
+
type: "reference",
|
|
10305
|
+
jsonbPath: "$.serviceProvider"
|
|
10306
|
+
},
|
|
10307
|
+
{ code: "appointment", type: "reference", jsonbPath: "$.appointment[*]" },
|
|
10308
|
+
{
|
|
10309
|
+
code: "episode-of-care",
|
|
10310
|
+
type: "reference",
|
|
10311
|
+
jsonbPath: "$.episodeOfCare[*]"
|
|
10197
10312
|
}
|
|
10198
|
-
|
|
10313
|
+
];
|
|
10314
|
+
|
|
10315
|
+
// src/data/search/registry/observation-search-parameters.ts
|
|
10316
|
+
var OBSERVATION_SEARCH_PARAMETERS = [
|
|
10317
|
+
{ code: "status", type: "token", jsonbPath: "$.status" },
|
|
10318
|
+
{ code: "category", type: "token", jsonbPath: "$.category[*]" },
|
|
10319
|
+
{ code: "code", type: "token", jsonbPath: "$.code" },
|
|
10320
|
+
{ code: "subject", type: "reference", jsonbPath: "$.subject" },
|
|
10321
|
+
{ code: "patient", type: "reference", jsonbPath: "$.subject" },
|
|
10322
|
+
{ code: "encounter", type: "reference", jsonbPath: "$.encounter" },
|
|
10323
|
+
{ code: "performer", type: "reference", jsonbPath: "$.performer[*]" },
|
|
10324
|
+
{ code: "date", type: "date", jsonbPath: "$.effectiveDateTime" },
|
|
10325
|
+
{
|
|
10326
|
+
code: "value-string",
|
|
10327
|
+
type: "string",
|
|
10328
|
+
jsonbPath: "$.valueString",
|
|
10329
|
+
modifiers: ["exact", "contains", "missing", "not"]
|
|
10330
|
+
},
|
|
10331
|
+
{ code: "identifier", type: "token", jsonbPath: "$.identifier[*]" },
|
|
10332
|
+
{ code: "based-on", type: "reference", jsonbPath: "$.basedOn[*]" },
|
|
10333
|
+
{ code: "part-of", type: "reference", jsonbPath: "$.partOf[*]" }
|
|
10334
|
+
];
|
|
10335
|
+
|
|
10336
|
+
// src/data/search/registry/patient-search-parameters.ts
|
|
10337
|
+
var PATIENT_SEARCH_PARAMETERS = [
|
|
10338
|
+
{ code: "gender", type: "token", jsonbPath: "$.gender" },
|
|
10339
|
+
{ code: "identifier", type: "token", jsonbPath: "$.identifier[*].value" },
|
|
10340
|
+
{ code: "birthdate", type: "date", jsonbPath: "$.birthDate" },
|
|
10341
|
+
{
|
|
10342
|
+
code: "name",
|
|
10343
|
+
type: "string",
|
|
10344
|
+
jsonbPath: "$.name[*].text",
|
|
10345
|
+
modifiers: ["exact", "contains", "missing", "not"]
|
|
10346
|
+
},
|
|
10347
|
+
{
|
|
10348
|
+
code: "family",
|
|
10349
|
+
type: "string",
|
|
10350
|
+
jsonbPath: "$.name[*].family",
|
|
10351
|
+
modifiers: ["exact", "contains", "missing", "not"]
|
|
10352
|
+
},
|
|
10353
|
+
{
|
|
10354
|
+
code: "given",
|
|
10355
|
+
type: "string",
|
|
10356
|
+
jsonbPath: "$.name[*].given",
|
|
10357
|
+
modifiers: ["exact", "contains", "missing", "not"]
|
|
10358
|
+
},
|
|
10359
|
+
{ code: "active", type: "token", jsonbPath: "$.active" },
|
|
10360
|
+
{ code: "deceased", type: "token", jsonbPath: "$.deceasedBoolean" },
|
|
10361
|
+
{
|
|
10362
|
+
code: "general-practitioner",
|
|
10363
|
+
type: "reference",
|
|
10364
|
+
jsonbPath: "$.generalPractitioner[*]"
|
|
10365
|
+
},
|
|
10366
|
+
{
|
|
10367
|
+
code: "organization",
|
|
10368
|
+
type: "reference",
|
|
10369
|
+
jsonbPath: "$.managingOrganization"
|
|
10370
|
+
}
|
|
10371
|
+
];
|
|
10372
|
+
|
|
10373
|
+
// src/data/search/registry/procedure-search-parameters.ts
|
|
10374
|
+
var PROCEDURE_SEARCH_PARAMETERS = [
|
|
10375
|
+
{ code: "status", type: "token", jsonbPath: "$.status" },
|
|
10376
|
+
{ code: "category", type: "token", jsonbPath: "$.category" },
|
|
10377
|
+
{ code: "code", type: "token", jsonbPath: "$.code" },
|
|
10378
|
+
{ code: "subject", type: "reference", jsonbPath: "$.subject" },
|
|
10379
|
+
{ code: "patient", type: "reference", jsonbPath: "$.subject" },
|
|
10380
|
+
{ code: "encounter", type: "reference", jsonbPath: "$.encounter" },
|
|
10381
|
+
{
|
|
10382
|
+
code: "performer",
|
|
10383
|
+
type: "reference",
|
|
10384
|
+
jsonbPath: "$.performer[*].actor"
|
|
10385
|
+
},
|
|
10386
|
+
{ code: "date", type: "date", jsonbPath: "$.performedDateTime" },
|
|
10387
|
+
{ code: "location", type: "reference", jsonbPath: "$.location" },
|
|
10388
|
+
{ code: "identifier", type: "token", jsonbPath: "$.identifier[*]" },
|
|
10389
|
+
{ code: "based-on", type: "reference", jsonbPath: "$.basedOn[*]" },
|
|
10390
|
+
{ code: "part-of", type: "reference", jsonbPath: "$.partOf[*]" },
|
|
10391
|
+
{ code: "reason-code", type: "token", jsonbPath: "$.reasonCode[*]" },
|
|
10392
|
+
{
|
|
10393
|
+
code: "reason-reference",
|
|
10394
|
+
type: "reference",
|
|
10395
|
+
jsonbPath: "$.reasonReference[*]"
|
|
10396
|
+
}
|
|
10397
|
+
];
|
|
10398
|
+
|
|
10399
|
+
// src/data/search/registry/resolver.ts
|
|
10400
|
+
var STATIC_SEARCH_PARAMETER_MAP = {
|
|
10401
|
+
Appointment: APPOINTMENT_SEARCH_PARAMETERS,
|
|
10402
|
+
Encounter: ENCOUNTER_SEARCH_PARAMETERS,
|
|
10403
|
+
Observation: OBSERVATION_SEARCH_PARAMETERS,
|
|
10404
|
+
Patient: PATIENT_SEARCH_PARAMETERS,
|
|
10405
|
+
Procedure: PROCEDURE_SEARCH_PARAMETERS
|
|
10406
|
+
};
|
|
10407
|
+
var defaultSearchParameterResolver = (resourceType, _tenantId) => STATIC_SEARCH_PARAMETER_MAP[resourceType] ?? [];
|
|
10408
|
+
function getRegisteredSearchParameters(resourceType) {
|
|
10409
|
+
return STATIC_SEARCH_PARAMETER_MAP[resourceType] ?? [];
|
|
10410
|
+
}
|
|
10411
|
+
|
|
10412
|
+
// src/data/rest-api/routes/data/appointment/appointment-list-route.ts
|
|
10413
|
+
var APPOINTMENT_RESOURCE_TYPE = "Appointment";
|
|
10414
|
+
function stripModifier(key) {
|
|
10415
|
+
const idx = key.indexOf(":");
|
|
10416
|
+
return idx === -1 ? key : key.slice(0, idx);
|
|
10417
|
+
}
|
|
10418
|
+
function isResultParameter(key) {
|
|
10419
|
+
return key.startsWith("_");
|
|
10199
10420
|
}
|
|
10200
10421
|
function sendInvalidSearch400(res, diagnostics) {
|
|
10201
10422
|
return res.status(400).json({
|
|
@@ -10203,95 +10424,93 @@ function sendInvalidSearch400(res, diagnostics) {
|
|
|
10203
10424
|
issue: [{ severity: "error", code: "invalid", diagnostics }]
|
|
10204
10425
|
});
|
|
10205
10426
|
}
|
|
10427
|
+
function extractSearchParamKeys(query) {
|
|
10428
|
+
const out = [];
|
|
10429
|
+
for (const rawKey of Object.keys(query)) {
|
|
10430
|
+
if (isResultParameter(rawKey)) {
|
|
10431
|
+
continue;
|
|
10432
|
+
}
|
|
10433
|
+
out.push({ rawKey, code: stripModifier(rawKey) });
|
|
10434
|
+
}
|
|
10435
|
+
return out;
|
|
10436
|
+
}
|
|
10437
|
+
function buildUnknownParamDiagnostics(unknownCodes) {
|
|
10438
|
+
const validCodes = getRegisteredSearchParameters(APPOINTMENT_RESOURCE_TYPE).map((p) => p.code).sort().join(", ");
|
|
10439
|
+
const codes = unknownCodes.join(", ");
|
|
10440
|
+
const isPlural = unknownCodes.length !== 1;
|
|
10441
|
+
return [
|
|
10442
|
+
`Unknown search ${isPlural ? "parameters" : "parameter"} for Appointment: ${codes}.`,
|
|
10443
|
+
`Valid codes: ${validCodes}.`
|
|
10444
|
+
].join(" ");
|
|
10445
|
+
}
|
|
10446
|
+
function findMalformedReference(query, searchParamKeys) {
|
|
10447
|
+
const referenceCodes = new Set(
|
|
10448
|
+
getRegisteredSearchParameters(APPOINTMENT_RESOURCE_TYPE).filter((p) => p.type === "reference").map((p) => p.code)
|
|
10449
|
+
);
|
|
10450
|
+
for (const { rawKey, code } of searchParamKeys) {
|
|
10451
|
+
if (!referenceCodes.has(code)) {
|
|
10452
|
+
continue;
|
|
10453
|
+
}
|
|
10454
|
+
const raw = query[rawKey];
|
|
10455
|
+
const values = typeof raw === "string" ? raw.split(",") : Array.isArray(raw) ? raw.flatMap((v) => v.split(",")) : [];
|
|
10456
|
+
for (const v of values) {
|
|
10457
|
+
const trimmed = v.trim();
|
|
10458
|
+
if (trimmed.length === 0) {
|
|
10459
|
+
continue;
|
|
10460
|
+
}
|
|
10461
|
+
if (parseTypedReference(trimmed) === void 0) {
|
|
10462
|
+
return { rawKey, value: trimmed };
|
|
10463
|
+
}
|
|
10464
|
+
}
|
|
10465
|
+
}
|
|
10466
|
+
return void 0;
|
|
10467
|
+
}
|
|
10206
10468
|
async function listAppointmentsRoute(req, res) {
|
|
10207
|
-
const
|
|
10208
|
-
|
|
10209
|
-
|
|
10210
|
-
if (
|
|
10211
|
-
return
|
|
10469
|
+
const searchParamKeys = extractSearchParamKeys(
|
|
10470
|
+
req.query
|
|
10471
|
+
);
|
|
10472
|
+
if (searchParamKeys.length === 0) {
|
|
10473
|
+
return handleListRoute({
|
|
10474
|
+
req,
|
|
10475
|
+
res,
|
|
10476
|
+
basePath: BASE_PATH.APPOINTMENT,
|
|
10477
|
+
listOperation: listAppointmentsOperation,
|
|
10478
|
+
errorLogContext: "GET /Appointment list error:"
|
|
10479
|
+
});
|
|
10212
10480
|
}
|
|
10213
|
-
const
|
|
10214
|
-
|
|
10481
|
+
const registered = getRegisteredSearchParameters(APPOINTMENT_RESOURCE_TYPE);
|
|
10482
|
+
const validCodes = new Set(registered.map((p) => p.code));
|
|
10483
|
+
const unknownCodes = searchParamKeys.map((k) => k.code).filter((code) => !validCodes.has(code));
|
|
10484
|
+
if (unknownCodes.length > 0) {
|
|
10215
10485
|
return sendInvalidSearch400(
|
|
10216
10486
|
res,
|
|
10217
|
-
|
|
10487
|
+
buildUnknownParamDiagnostics([...new Set(unknownCodes)])
|
|
10218
10488
|
);
|
|
10219
10489
|
}
|
|
10220
|
-
|
|
10221
|
-
|
|
10222
|
-
|
|
10223
|
-
|
|
10224
|
-
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
const result = await searchAppointmentsByActorOperation({
|
|
10230
|
-
context: ctx,
|
|
10231
|
-
actorReference: actorRef,
|
|
10232
|
-
dateConstraints
|
|
10233
|
-
});
|
|
10234
|
-
const bundle = buildSearchsetBundle(
|
|
10235
|
-
BASE_PATH.APPOINTMENT,
|
|
10236
|
-
result.entries
|
|
10237
|
-
);
|
|
10238
|
-
return res.json(bundle);
|
|
10239
|
-
} catch (err) {
|
|
10240
|
-
return sendOperationOutcome500(
|
|
10241
|
-
res,
|
|
10242
|
-
err,
|
|
10243
|
-
"GET /Appointment?actor= search error:"
|
|
10244
|
-
);
|
|
10245
|
-
}
|
|
10246
|
-
}
|
|
10247
|
-
if (patientId !== void 0) {
|
|
10248
|
-
const ctx = req.openhiContext;
|
|
10249
|
-
try {
|
|
10250
|
-
const result = await searchAppointmentsByPatientOperation({
|
|
10251
|
-
context: ctx,
|
|
10252
|
-
patientId,
|
|
10253
|
-
dateConstraints
|
|
10254
|
-
});
|
|
10255
|
-
const bundle = buildSearchsetBundle(
|
|
10256
|
-
BASE_PATH.APPOINTMENT,
|
|
10257
|
-
result.entries
|
|
10258
|
-
);
|
|
10259
|
-
return res.json(bundle);
|
|
10260
|
-
} catch (err) {
|
|
10261
|
-
return sendOperationOutcome500(
|
|
10262
|
-
res,
|
|
10263
|
-
err,
|
|
10264
|
-
"GET /Appointment?patient= search error:"
|
|
10265
|
-
);
|
|
10266
|
-
}
|
|
10490
|
+
const malformedRef = findMalformedReference(
|
|
10491
|
+
req.query,
|
|
10492
|
+
searchParamKeys
|
|
10493
|
+
);
|
|
10494
|
+
if (malformedRef !== void 0) {
|
|
10495
|
+
return sendInvalidSearch400(
|
|
10496
|
+
res,
|
|
10497
|
+
`?${malformedRef.rawKey} must be a typed reference like "Practitioner/<id>"; got "${malformedRef.value}".`
|
|
10498
|
+
);
|
|
10267
10499
|
}
|
|
10268
|
-
|
|
10269
|
-
|
|
10270
|
-
|
|
10271
|
-
|
|
10272
|
-
|
|
10273
|
-
|
|
10274
|
-
|
|
10275
|
-
|
|
10276
|
-
|
|
10277
|
-
|
|
10278
|
-
|
|
10279
|
-
|
|
10280
|
-
|
|
10281
|
-
return sendOperationOutcome500(
|
|
10282
|
-
res,
|
|
10283
|
-
err,
|
|
10284
|
-
"GET /Appointment?date= search error:"
|
|
10285
|
-
);
|
|
10286
|
-
}
|
|
10500
|
+
const ctx = req.openhiContext;
|
|
10501
|
+
try {
|
|
10502
|
+
const result = await genericSearchOperation({
|
|
10503
|
+
resourceType: APPOINTMENT_RESOURCE_TYPE,
|
|
10504
|
+
tenantId: ctx.tenantId,
|
|
10505
|
+
workspaceId: ctx.workspaceId,
|
|
10506
|
+
query: req.query,
|
|
10507
|
+
resolver: defaultSearchParameterResolver
|
|
10508
|
+
});
|
|
10509
|
+
const bundle = buildSearchsetBundle(BASE_PATH.APPOINTMENT, result.entries);
|
|
10510
|
+
return res.json(bundle);
|
|
10511
|
+
} catch (err) {
|
|
10512
|
+
return sendOperationOutcome500(res, err, "GET /Appointment search error:");
|
|
10287
10513
|
}
|
|
10288
|
-
return handleListRoute({
|
|
10289
|
-
req,
|
|
10290
|
-
res,
|
|
10291
|
-
basePath: BASE_PATH.APPOINTMENT,
|
|
10292
|
-
listOperation: listAppointmentsOperation,
|
|
10293
|
-
errorLogContext: "GET /Appointment list error:"
|
|
10294
|
-
});
|
|
10295
10514
|
}
|
|
10296
10515
|
|
|
10297
10516
|
// src/data/operations/data/appointment/appointment-update-operation.ts
|
|
@@ -17834,353 +18053,108 @@ async function listEncountersOperation(params) {
|
|
|
17834
18053
|
);
|
|
17835
18054
|
}
|
|
17836
18055
|
|
|
17837
|
-
// src/data/
|
|
17838
|
-
var
|
|
17839
|
-
|
|
17840
|
-
"
|
|
17841
|
-
|
|
17842
|
-
"le",
|
|
17843
|
-
"sa",
|
|
17844
|
-
"eb"
|
|
17845
|
-
];
|
|
17846
|
-
function isPeriodSearchPrefix(s) {
|
|
17847
|
-
return PERIOD_SEARCH_PREFIXES.includes(s);
|
|
18056
|
+
// src/data/rest-api/routes/data/encounter/encounter-list-route.ts
|
|
18057
|
+
var ENCOUNTER_RESOURCE_TYPE = "Encounter";
|
|
18058
|
+
function stripModifier2(key) {
|
|
18059
|
+
const idx = key.indexOf(":");
|
|
18060
|
+
return idx === -1 ? key : key.slice(0, idx);
|
|
17848
18061
|
}
|
|
17849
|
-
|
|
17850
|
-
|
|
17851
|
-
var HAS_ANY_BOUND_GUARD = `(${PERIOD_START} IS NOT NULL OR ${PERIOD_END} IS NOT NULL)`;
|
|
17852
|
-
function buildSinglePredicateSql(prefix, paramName) {
|
|
17853
|
-
switch (prefix) {
|
|
17854
|
-
case "gt":
|
|
17855
|
-
return `(${PERIOD_END} IS NULL OR ${PERIOD_END} > :${paramName})`;
|
|
17856
|
-
case "lt":
|
|
17857
|
-
return `(${PERIOD_START} IS NULL OR ${PERIOD_START} < :${paramName})`;
|
|
17858
|
-
case "ge":
|
|
17859
|
-
return `(${PERIOD_END} IS NULL OR ${PERIOD_END} >= :${paramName})`;
|
|
17860
|
-
case "le":
|
|
17861
|
-
return `(${PERIOD_START} IS NULL OR ${PERIOD_START} <= :${paramName})`;
|
|
17862
|
-
case "sa":
|
|
17863
|
-
return `(${PERIOD_START} IS NOT NULL AND ${PERIOD_START} > :${paramName})`;
|
|
17864
|
-
case "eb":
|
|
17865
|
-
return `(${PERIOD_END} IS NOT NULL AND ${PERIOD_END} < :${paramName})`;
|
|
17866
|
-
}
|
|
18062
|
+
function isResultParameter2(key) {
|
|
18063
|
+
return key.startsWith("_");
|
|
17867
18064
|
}
|
|
17868
|
-
function
|
|
17869
|
-
return
|
|
18065
|
+
function sendInvalidSearch4002(res, diagnostics) {
|
|
18066
|
+
return res.status(400).json({
|
|
18067
|
+
resourceType: "OperationOutcome",
|
|
18068
|
+
issue: [{ severity: "error", code: "invalid", diagnostics }]
|
|
18069
|
+
});
|
|
17870
18070
|
}
|
|
17871
|
-
function
|
|
17872
|
-
|
|
17873
|
-
|
|
18071
|
+
function extractSearchParamKeys2(query) {
|
|
18072
|
+
const out = [];
|
|
18073
|
+
for (const rawKey of Object.keys(query)) {
|
|
18074
|
+
if (isResultParameter2(rawKey)) {
|
|
18075
|
+
continue;
|
|
18076
|
+
}
|
|
18077
|
+
out.push({ rawKey, code: stripModifier2(rawKey) });
|
|
17874
18078
|
}
|
|
17875
|
-
|
|
17876
|
-
(c, i) => buildSinglePredicateSql(c.prefix, periodConstraintParamName(i))
|
|
17877
|
-
);
|
|
17878
|
-
fragments.push(HAS_ANY_BOUND_GUARD);
|
|
17879
|
-
return fragments;
|
|
18079
|
+
return out;
|
|
17880
18080
|
}
|
|
17881
|
-
function
|
|
17882
|
-
|
|
17883
|
-
|
|
17884
|
-
|
|
17885
|
-
|
|
18081
|
+
function buildUnknownParamDiagnostics2(unknownCodes) {
|
|
18082
|
+
const validCodes = getRegisteredSearchParameters(ENCOUNTER_RESOURCE_TYPE).map((p) => p.code).sort().join(", ");
|
|
18083
|
+
const codes = unknownCodes.join(", ");
|
|
18084
|
+
const isPlural = unknownCodes.length !== 1;
|
|
18085
|
+
return [
|
|
18086
|
+
`Unknown search ${isPlural ? "parameters" : "parameter"} for Encounter: ${codes}.`,
|
|
18087
|
+
`Valid codes: ${validCodes}.`
|
|
18088
|
+
].join(" ");
|
|
17886
18089
|
}
|
|
17887
|
-
|
|
17888
|
-
|
|
17889
|
-
|
|
17890
|
-
function buildSearchEncountersByDateSql(opts) {
|
|
17891
|
-
const periodPredicates = buildPeriodSearchPredicateSql(
|
|
17892
|
-
opts.periodConstraints
|
|
18090
|
+
function findMalformedReference2(query, searchParamKeys) {
|
|
18091
|
+
const referenceCodes = new Set(
|
|
18092
|
+
getRegisteredSearchParameters(ENCOUNTER_RESOURCE_TYPE).filter((p) => p.type === "reference").map((p) => p.code)
|
|
17893
18093
|
);
|
|
17894
|
-
const
|
|
17895
|
-
|
|
17896
|
-
|
|
17897
|
-
"WHERE tenant_id = :tenantId",
|
|
17898
|
-
" AND workspace_id = :workspaceId",
|
|
17899
|
-
" AND resource_type = 'Encounter'",
|
|
17900
|
-
" AND deleted_at IS NULL"
|
|
17901
|
-
];
|
|
17902
|
-
for (const fragment of periodPredicates) {
|
|
17903
|
-
lines.push(` AND ${fragment}`);
|
|
17904
|
-
}
|
|
17905
|
-
lines.push("ORDER BY last_updated DESC");
|
|
17906
|
-
lines.push("LIMIT :limit;");
|
|
17907
|
-
return lines.join("\n");
|
|
17908
|
-
}
|
|
17909
|
-
async function searchEncountersByDateOperation(params) {
|
|
17910
|
-
const { context, periodConstraints } = params;
|
|
17911
|
-
if (periodConstraints.length === 0) {
|
|
17912
|
-
throw new Error(
|
|
17913
|
-
"searchEncountersByDateOperation requires at least one periodConstraint"
|
|
17914
|
-
);
|
|
17915
|
-
}
|
|
17916
|
-
const { tenantId, workspaceId } = context;
|
|
17917
|
-
const runner = params.runner ?? getDefaultPostgresQueryRunner();
|
|
17918
|
-
const limit = params.limit ?? DEFAULT_LIMIT4;
|
|
17919
|
-
const sql = buildSearchEncountersByDateSql({ periodConstraints });
|
|
17920
|
-
const queryParams = [
|
|
17921
|
-
{ name: "tenantId", value: tenantId },
|
|
17922
|
-
{ name: "workspaceId", value: workspaceId },
|
|
17923
|
-
{ name: "limit", value: limit },
|
|
17924
|
-
...buildPeriodSearchPredicateParams(periodConstraints)
|
|
17925
|
-
];
|
|
17926
|
-
const rows = await runner.query(sql, queryParams);
|
|
17927
|
-
const entries = rows.map((row) => ({
|
|
17928
|
-
id: row.id,
|
|
17929
|
-
resource: {
|
|
17930
|
-
...row.resource,
|
|
17931
|
-
id: row.id
|
|
17932
|
-
}
|
|
17933
|
-
}));
|
|
17934
|
-
return { entries, total: entries.length };
|
|
17935
|
-
}
|
|
17936
|
-
|
|
17937
|
-
// src/data/operations/data/encounter/encounter-search-by-participant-operation.ts
|
|
17938
|
-
var DEFAULT_LIMIT5 = 100;
|
|
17939
|
-
function buildSearchEncountersByParticipantSql(opts) {
|
|
17940
|
-
const periodPredicates = buildPeriodSearchPredicateSql(
|
|
17941
|
-
opts?.periodConstraints ?? []
|
|
17942
|
-
);
|
|
17943
|
-
const lines = [
|
|
17944
|
-
"SELECT resource_id AS id, resource",
|
|
17945
|
-
"FROM resources",
|
|
17946
|
-
"WHERE tenant_id = :tenantId",
|
|
17947
|
-
" AND workspace_id = :workspaceId",
|
|
17948
|
-
" AND resource_type = 'Encounter'",
|
|
17949
|
-
" AND deleted_at IS NULL",
|
|
17950
|
-
` AND ${REFERENCE_CONTAINMENT_SQL_FRAGMENT}`
|
|
17951
|
-
];
|
|
17952
|
-
for (const fragment of periodPredicates) {
|
|
17953
|
-
lines.push(` AND ${fragment}`);
|
|
17954
|
-
}
|
|
17955
|
-
lines.push("ORDER BY last_updated DESC");
|
|
17956
|
-
lines.push("LIMIT :limit;");
|
|
17957
|
-
return lines.join("\n");
|
|
17958
|
-
}
|
|
17959
|
-
async function searchEncountersByParticipantOperation(params) {
|
|
17960
|
-
const { context, participantReference } = params;
|
|
17961
|
-
const periodConstraints = params.periodConstraints ?? [];
|
|
17962
|
-
const { tenantId, workspaceId } = context;
|
|
17963
|
-
const runner = params.runner ?? getDefaultPostgresQueryRunner();
|
|
17964
|
-
const limit = params.limit ?? DEFAULT_LIMIT5;
|
|
17965
|
-
const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
|
|
17966
|
-
shape: {
|
|
17967
|
-
kind: "array-of-objects",
|
|
17968
|
-
field: "participant",
|
|
17969
|
-
subfield: "individual"
|
|
17970
|
-
},
|
|
17971
|
-
reference: participantReference,
|
|
17972
|
-
tenantId,
|
|
17973
|
-
workspaceId
|
|
17974
|
-
});
|
|
17975
|
-
const sql = buildSearchEncountersByParticipantSql({ periodConstraints });
|
|
17976
|
-
const queryParams = [
|
|
17977
|
-
{ name: "tenantId", value: tenantId },
|
|
17978
|
-
{ name: "workspaceId", value: workspaceId },
|
|
17979
|
-
{ name: "containmentRelative", value: containmentRelative },
|
|
17980
|
-
{ name: "containmentUrn", value: containmentUrn },
|
|
17981
|
-
{ name: "limit", value: limit },
|
|
17982
|
-
...buildPeriodSearchPredicateParams(periodConstraints)
|
|
17983
|
-
];
|
|
17984
|
-
const rows = await runner.query(sql, queryParams);
|
|
17985
|
-
const entries = rows.map((row) => ({
|
|
17986
|
-
id: row.id,
|
|
17987
|
-
resource: {
|
|
17988
|
-
...row.resource,
|
|
17989
|
-
id: row.id
|
|
17990
|
-
}
|
|
17991
|
-
}));
|
|
17992
|
-
return { entries, total: entries.length };
|
|
17993
|
-
}
|
|
17994
|
-
|
|
17995
|
-
// src/data/operations/data/encounter/encounter-search-by-patient-operation.ts
|
|
17996
|
-
var DEFAULT_LIMIT6 = 100;
|
|
17997
|
-
function buildSearchEncountersByPatientSql(opts) {
|
|
17998
|
-
const periodPredicates = buildPeriodSearchPredicateSql(
|
|
17999
|
-
opts?.periodConstraints ?? []
|
|
18000
|
-
);
|
|
18001
|
-
const lines = [
|
|
18002
|
-
"SELECT resource_id AS id, resource",
|
|
18003
|
-
"FROM resources",
|
|
18004
|
-
"WHERE tenant_id = :tenantId",
|
|
18005
|
-
" AND workspace_id = :workspaceId",
|
|
18006
|
-
" AND resource_type = 'Encounter'",
|
|
18007
|
-
" AND deleted_at IS NULL",
|
|
18008
|
-
" AND (resource @> :containmentRelative::jsonb",
|
|
18009
|
-
" OR resource @> :containmentUrn::jsonb)"
|
|
18010
|
-
];
|
|
18011
|
-
for (const fragment of periodPredicates) {
|
|
18012
|
-
lines.push(` AND ${fragment}`);
|
|
18013
|
-
}
|
|
18014
|
-
lines.push("ORDER BY last_updated DESC");
|
|
18015
|
-
lines.push("LIMIT :limit;");
|
|
18016
|
-
return lines.join("\n");
|
|
18017
|
-
}
|
|
18018
|
-
async function searchEncountersByPatientOperation(params) {
|
|
18019
|
-
const { context, patientId } = params;
|
|
18020
|
-
const periodConstraints = params.periodConstraints ?? [];
|
|
18021
|
-
const { tenantId, workspaceId } = context;
|
|
18022
|
-
const runner = params.runner ?? getDefaultPostgresQueryRunner();
|
|
18023
|
-
const limit = params.limit ?? DEFAULT_LIMIT6;
|
|
18024
|
-
const containmentRelative = JSON.stringify({
|
|
18025
|
-
subject: { reference: `Patient/${patientId}` }
|
|
18026
|
-
});
|
|
18027
|
-
const containmentUrn = JSON.stringify({
|
|
18028
|
-
subject: {
|
|
18029
|
-
reference: buildOpenHiResourceUrn({
|
|
18030
|
-
tenantId,
|
|
18031
|
-
workspaceId,
|
|
18032
|
-
resourceType: "Patient",
|
|
18033
|
-
resourceId: patientId
|
|
18034
|
-
})
|
|
18035
|
-
}
|
|
18036
|
-
});
|
|
18037
|
-
const sql = buildSearchEncountersByPatientSql({ periodConstraints });
|
|
18038
|
-
const queryParams = [
|
|
18039
|
-
{ name: "tenantId", value: tenantId },
|
|
18040
|
-
{ name: "workspaceId", value: workspaceId },
|
|
18041
|
-
{ name: "containmentRelative", value: containmentRelative },
|
|
18042
|
-
{ name: "containmentUrn", value: containmentUrn },
|
|
18043
|
-
{ name: "limit", value: limit },
|
|
18044
|
-
...buildPeriodSearchPredicateParams(periodConstraints)
|
|
18045
|
-
];
|
|
18046
|
-
const rows = await runner.query(sql, queryParams);
|
|
18047
|
-
const entries = rows.map((row) => ({
|
|
18048
|
-
id: row.id,
|
|
18049
|
-
resource: {
|
|
18050
|
-
...row.resource,
|
|
18051
|
-
id: row.id
|
|
18052
|
-
}
|
|
18053
|
-
}));
|
|
18054
|
-
return { entries, total: entries.length };
|
|
18055
|
-
}
|
|
18056
|
-
|
|
18057
|
-
// src/data/rest-api/routes/data/encounter/encounter-list-route.ts
|
|
18058
|
-
function singleStringQueryParam2(req, name) {
|
|
18059
|
-
const v = req.query[name];
|
|
18060
|
-
if (typeof v !== "string") {
|
|
18061
|
-
return void 0;
|
|
18062
|
-
}
|
|
18063
|
-
const trimmed = v.trim();
|
|
18064
|
-
return trimmed === "" ? void 0 : trimmed;
|
|
18065
|
-
}
|
|
18066
|
-
function isError2(v) {
|
|
18067
|
-
return v.error !== void 0;
|
|
18068
|
-
}
|
|
18069
|
-
function parseEncounterDateConstraints(req) {
|
|
18070
|
-
const raw = req.query.date;
|
|
18071
|
-
if (raw === void 0) {
|
|
18072
|
-
return [];
|
|
18073
|
-
}
|
|
18074
|
-
const values = Array.isArray(raw) ? raw : [raw];
|
|
18075
|
-
const out = [];
|
|
18076
|
-
for (const v of values) {
|
|
18077
|
-
if (typeof v !== "string") {
|
|
18078
|
-
return { error: "Each ?date= value must be a string." };
|
|
18079
|
-
}
|
|
18080
|
-
const trimmed = v.trim();
|
|
18081
|
-
if (trimmed === "") {
|
|
18082
|
-
return { error: "?date= value must not be empty." };
|
|
18083
|
-
}
|
|
18084
|
-
const prefix = trimmed.slice(0, 2);
|
|
18085
|
-
const datetime = trimmed.slice(2);
|
|
18086
|
-
if (!isPeriodSearchPrefix(prefix)) {
|
|
18087
|
-
return {
|
|
18088
|
-
error: `Unsupported ?date= prefix in "${trimmed}". Supported prefixes: ${PERIOD_SEARCH_PREFIXES.join(", ")}.`
|
|
18089
|
-
};
|
|
18094
|
+
for (const { rawKey, code } of searchParamKeys) {
|
|
18095
|
+
if (!referenceCodes.has(code)) {
|
|
18096
|
+
continue;
|
|
18090
18097
|
}
|
|
18091
|
-
|
|
18092
|
-
|
|
18098
|
+
const raw = query[rawKey];
|
|
18099
|
+
const values = typeof raw === "string" ? raw.split(",") : Array.isArray(raw) ? raw.flatMap((v) => v.split(",")) : [];
|
|
18100
|
+
for (const v of values) {
|
|
18101
|
+
const trimmed = v.trim();
|
|
18102
|
+
if (trimmed.length === 0) {
|
|
18103
|
+
continue;
|
|
18104
|
+
}
|
|
18105
|
+
if (parseTypedReference(trimmed) === void 0) {
|
|
18106
|
+
return { rawKey, value: trimmed };
|
|
18107
|
+
}
|
|
18093
18108
|
}
|
|
18094
|
-
out.push({ prefix, value: datetime });
|
|
18095
18109
|
}
|
|
18096
|
-
return
|
|
18097
|
-
}
|
|
18098
|
-
function sendInvalidSearch4002(res, diagnostics) {
|
|
18099
|
-
return res.status(400).json({
|
|
18100
|
-
resourceType: "OperationOutcome",
|
|
18101
|
-
issue: [{ severity: "error", code: "invalid", diagnostics }]
|
|
18102
|
-
});
|
|
18110
|
+
return void 0;
|
|
18103
18111
|
}
|
|
18104
18112
|
async function listEncountersRoute(req, res) {
|
|
18105
|
-
const
|
|
18106
|
-
|
|
18107
|
-
|
|
18108
|
-
if (
|
|
18109
|
-
return
|
|
18110
|
-
|
|
18111
|
-
|
|
18112
|
-
|
|
18113
|
+
const searchParamKeys = extractSearchParamKeys2(
|
|
18114
|
+
req.query
|
|
18115
|
+
);
|
|
18116
|
+
if (searchParamKeys.length === 0) {
|
|
18117
|
+
return handleListRoute({
|
|
18118
|
+
req,
|
|
18119
|
+
res,
|
|
18120
|
+
basePath: BASE_PATH.ENCOUNTER,
|
|
18121
|
+
listOperation: listEncountersOperation,
|
|
18122
|
+
errorLogContext: "GET /Encounter list error:"
|
|
18123
|
+
});
|
|
18124
|
+
}
|
|
18125
|
+
const registered = getRegisteredSearchParameters(ENCOUNTER_RESOURCE_TYPE);
|
|
18126
|
+
const validCodes = new Set(registered.map((p) => p.code));
|
|
18127
|
+
const unknownCodes = searchParamKeys.map((k) => k.code).filter((code) => !validCodes.has(code));
|
|
18128
|
+
if (unknownCodes.length > 0) {
|
|
18113
18129
|
return sendInvalidSearch4002(
|
|
18114
18130
|
res,
|
|
18115
|
-
|
|
18131
|
+
buildUnknownParamDiagnostics2([...new Set(unknownCodes)])
|
|
18116
18132
|
);
|
|
18117
18133
|
}
|
|
18118
|
-
|
|
18119
|
-
|
|
18120
|
-
|
|
18121
|
-
|
|
18122
|
-
|
|
18123
|
-
|
|
18124
|
-
|
|
18125
|
-
|
|
18126
|
-
|
|
18127
|
-
const result = await searchEncountersByParticipantOperation({
|
|
18128
|
-
context: ctx,
|
|
18129
|
-
participantReference: participantRef,
|
|
18130
|
-
periodConstraints
|
|
18131
|
-
});
|
|
18132
|
-
const bundle = buildSearchsetBundle(BASE_PATH.ENCOUNTER, result.entries);
|
|
18133
|
-
return res.json(bundle);
|
|
18134
|
-
} catch (err) {
|
|
18135
|
-
return sendOperationOutcome500(
|
|
18136
|
-
res,
|
|
18137
|
-
err,
|
|
18138
|
-
"GET /Encounter?participant= search error:"
|
|
18139
|
-
);
|
|
18140
|
-
}
|
|
18141
|
-
}
|
|
18142
|
-
if (patientId !== void 0) {
|
|
18143
|
-
const ctx = req.openhiContext;
|
|
18144
|
-
try {
|
|
18145
|
-
const result = await searchEncountersByPatientOperation({
|
|
18146
|
-
context: ctx,
|
|
18147
|
-
patientId,
|
|
18148
|
-
periodConstraints
|
|
18149
|
-
});
|
|
18150
|
-
const bundle = buildSearchsetBundle(BASE_PATH.ENCOUNTER, result.entries);
|
|
18151
|
-
return res.json(bundle);
|
|
18152
|
-
} catch (err) {
|
|
18153
|
-
return sendOperationOutcome500(
|
|
18154
|
-
res,
|
|
18155
|
-
err,
|
|
18156
|
-
"GET /Encounter?patient= search error:"
|
|
18157
|
-
);
|
|
18158
|
-
}
|
|
18134
|
+
const malformedRef = findMalformedReference2(
|
|
18135
|
+
req.query,
|
|
18136
|
+
searchParamKeys
|
|
18137
|
+
);
|
|
18138
|
+
if (malformedRef !== void 0) {
|
|
18139
|
+
return sendInvalidSearch4002(
|
|
18140
|
+
res,
|
|
18141
|
+
`?${malformedRef.rawKey} must be a typed reference like "Practitioner/<id>"; got "${malformedRef.value}".`
|
|
18142
|
+
);
|
|
18159
18143
|
}
|
|
18160
|
-
|
|
18161
|
-
|
|
18162
|
-
|
|
18163
|
-
|
|
18164
|
-
|
|
18165
|
-
|
|
18166
|
-
|
|
18167
|
-
|
|
18168
|
-
|
|
18169
|
-
|
|
18170
|
-
|
|
18171
|
-
|
|
18172
|
-
|
|
18173
|
-
"GET /Encounter?date= search error:"
|
|
18174
|
-
);
|
|
18175
|
-
}
|
|
18144
|
+
const ctx = req.openhiContext;
|
|
18145
|
+
try {
|
|
18146
|
+
const result = await genericSearchOperation({
|
|
18147
|
+
resourceType: ENCOUNTER_RESOURCE_TYPE,
|
|
18148
|
+
tenantId: ctx.tenantId,
|
|
18149
|
+
workspaceId: ctx.workspaceId,
|
|
18150
|
+
query: req.query,
|
|
18151
|
+
resolver: defaultSearchParameterResolver
|
|
18152
|
+
});
|
|
18153
|
+
const bundle = buildSearchsetBundle(BASE_PATH.ENCOUNTER, result.entries);
|
|
18154
|
+
return res.json(bundle);
|
|
18155
|
+
} catch (err) {
|
|
18156
|
+
return sendOperationOutcome500(res, err, "GET /Encounter search error:");
|
|
18176
18157
|
}
|
|
18177
|
-
return handleListRoute({
|
|
18178
|
-
req,
|
|
18179
|
-
res,
|
|
18180
|
-
basePath: BASE_PATH.ENCOUNTER,
|
|
18181
|
-
listOperation: listEncountersOperation,
|
|
18182
|
-
errorLogContext: "GET /Encounter list error:"
|
|
18183
|
-
});
|
|
18184
18158
|
}
|
|
18185
18159
|
|
|
18186
18160
|
// src/data/operations/data/encounter/encounter-update-operation.ts
|
|
@@ -28895,44 +28869,137 @@ async function listObservationsOperation(params) {
|
|
|
28895
28869
|
}
|
|
28896
28870
|
|
|
28897
28871
|
// src/data/rest-api/routes/data/observation/observation-list-route.ts
|
|
28898
|
-
|
|
28899
|
-
|
|
28900
|
-
|
|
28901
|
-
|
|
28902
|
-
|
|
28903
|
-
|
|
28904
|
-
|
|
28872
|
+
var OBSERVATION_RESOURCE_TYPE = "Observation";
|
|
28873
|
+
function stripModifier3(key) {
|
|
28874
|
+
const idx = key.indexOf(":");
|
|
28875
|
+
return idx === -1 ? key : key.slice(0, idx);
|
|
28876
|
+
}
|
|
28877
|
+
function isResultParameter3(key) {
|
|
28878
|
+
return key.startsWith("_");
|
|
28879
|
+
}
|
|
28880
|
+
function sendInvalidSearch4003(res, diagnostics) {
|
|
28881
|
+
return res.status(400).json({
|
|
28882
|
+
resourceType: "OperationOutcome",
|
|
28883
|
+
issue: [{ severity: "error", code: "invalid", diagnostics }]
|
|
28905
28884
|
});
|
|
28906
28885
|
}
|
|
28907
|
-
|
|
28908
|
-
|
|
28909
|
-
|
|
28910
|
-
|
|
28911
|
-
|
|
28912
|
-
|
|
28913
|
-
|
|
28914
|
-
|
|
28915
|
-
|
|
28916
|
-
workspaceId,
|
|
28917
|
-
id,
|
|
28918
|
-
"Observation",
|
|
28919
|
-
context,
|
|
28920
|
-
(existingResourceStr) => buildUpdatedResourceWithAudit(
|
|
28921
|
-
body,
|
|
28922
|
-
id,
|
|
28923
|
-
date,
|
|
28924
|
-
actorId,
|
|
28925
|
-
actorName,
|
|
28926
|
-
existingResourceStr,
|
|
28927
|
-
"Observation"
|
|
28928
|
-
)
|
|
28929
|
-
);
|
|
28886
|
+
function extractSearchParamKeys3(query) {
|
|
28887
|
+
const out = [];
|
|
28888
|
+
for (const rawKey of Object.keys(query)) {
|
|
28889
|
+
if (isResultParameter3(rawKey)) {
|
|
28890
|
+
continue;
|
|
28891
|
+
}
|
|
28892
|
+
out.push({ rawKey, code: stripModifier3(rawKey) });
|
|
28893
|
+
}
|
|
28894
|
+
return out;
|
|
28930
28895
|
}
|
|
28931
|
-
|
|
28932
|
-
|
|
28933
|
-
|
|
28934
|
-
const
|
|
28935
|
-
|
|
28896
|
+
function buildUnknownParamDiagnostics3(unknownCodes) {
|
|
28897
|
+
const validCodes = getRegisteredSearchParameters(OBSERVATION_RESOURCE_TYPE).map((p) => p.code).sort().join(", ");
|
|
28898
|
+
const codes = unknownCodes.join(", ");
|
|
28899
|
+
const isPlural = unknownCodes.length !== 1;
|
|
28900
|
+
return [
|
|
28901
|
+
`Unknown search ${isPlural ? "parameters" : "parameter"} for Observation: ${codes}.`,
|
|
28902
|
+
`Valid codes: ${validCodes}.`
|
|
28903
|
+
].join(" ");
|
|
28904
|
+
}
|
|
28905
|
+
function findMalformedReference3(query, searchParamKeys) {
|
|
28906
|
+
const referenceCodes = new Set(
|
|
28907
|
+
getRegisteredSearchParameters(OBSERVATION_RESOURCE_TYPE).filter((p) => p.type === "reference").map((p) => p.code)
|
|
28908
|
+
);
|
|
28909
|
+
for (const { rawKey, code } of searchParamKeys) {
|
|
28910
|
+
if (!referenceCodes.has(code)) {
|
|
28911
|
+
continue;
|
|
28912
|
+
}
|
|
28913
|
+
const raw = query[rawKey];
|
|
28914
|
+
const values = typeof raw === "string" ? raw.split(",") : Array.isArray(raw) ? raw.flatMap((v) => v.split(",")) : [];
|
|
28915
|
+
for (const v of values) {
|
|
28916
|
+
const trimmed = v.trim();
|
|
28917
|
+
if (trimmed.length === 0) {
|
|
28918
|
+
continue;
|
|
28919
|
+
}
|
|
28920
|
+
if (parseTypedReference(trimmed) === void 0) {
|
|
28921
|
+
return { rawKey, value: trimmed };
|
|
28922
|
+
}
|
|
28923
|
+
}
|
|
28924
|
+
}
|
|
28925
|
+
return void 0;
|
|
28926
|
+
}
|
|
28927
|
+
async function listObservationsRoute(req, res) {
|
|
28928
|
+
const searchParamKeys = extractSearchParamKeys3(
|
|
28929
|
+
req.query
|
|
28930
|
+
);
|
|
28931
|
+
if (searchParamKeys.length === 0) {
|
|
28932
|
+
return handleListRoute({
|
|
28933
|
+
req,
|
|
28934
|
+
res,
|
|
28935
|
+
basePath: BASE_PATH.OBSERVATION,
|
|
28936
|
+
listOperation: listObservationsOperation,
|
|
28937
|
+
errorLogContext: "GET /Observation list error:"
|
|
28938
|
+
});
|
|
28939
|
+
}
|
|
28940
|
+
const registered = getRegisteredSearchParameters(OBSERVATION_RESOURCE_TYPE);
|
|
28941
|
+
const validCodes = new Set(registered.map((p) => p.code));
|
|
28942
|
+
const unknownCodes = searchParamKeys.map((k) => k.code).filter((code) => !validCodes.has(code));
|
|
28943
|
+
if (unknownCodes.length > 0) {
|
|
28944
|
+
return sendInvalidSearch4003(
|
|
28945
|
+
res,
|
|
28946
|
+
buildUnknownParamDiagnostics3([...new Set(unknownCodes)])
|
|
28947
|
+
);
|
|
28948
|
+
}
|
|
28949
|
+
const malformedRef = findMalformedReference3(
|
|
28950
|
+
req.query,
|
|
28951
|
+
searchParamKeys
|
|
28952
|
+
);
|
|
28953
|
+
if (malformedRef !== void 0) {
|
|
28954
|
+
return sendInvalidSearch4003(
|
|
28955
|
+
res,
|
|
28956
|
+
`?${malformedRef.rawKey} must be a typed reference like "Practitioner/<id>"; got "${malformedRef.value}".`
|
|
28957
|
+
);
|
|
28958
|
+
}
|
|
28959
|
+
const ctx = req.openhiContext;
|
|
28960
|
+
try {
|
|
28961
|
+
const result = await genericSearchOperation({
|
|
28962
|
+
resourceType: OBSERVATION_RESOURCE_TYPE,
|
|
28963
|
+
tenantId: ctx.tenantId,
|
|
28964
|
+
workspaceId: ctx.workspaceId,
|
|
28965
|
+
query: req.query,
|
|
28966
|
+
resolver: defaultSearchParameterResolver
|
|
28967
|
+
});
|
|
28968
|
+
const bundle = buildSearchsetBundle(BASE_PATH.OBSERVATION, result.entries);
|
|
28969
|
+
return res.json(bundle);
|
|
28970
|
+
} catch (err) {
|
|
28971
|
+
return sendOperationOutcome500(res, err, "GET /Observation search error:");
|
|
28972
|
+
}
|
|
28973
|
+
}
|
|
28974
|
+
|
|
28975
|
+
// src/data/operations/data/observation/observation-update-operation.ts
|
|
28976
|
+
async function updateObservationOperation(params) {
|
|
28977
|
+
const { context, id, body, tableName } = params;
|
|
28978
|
+
const { tenantId, workspaceId, date, actorId, actorName } = context;
|
|
28979
|
+
const service = getDynamoDataService(tableName);
|
|
28980
|
+
return updateDataEntityById(
|
|
28981
|
+
service.entities.observation,
|
|
28982
|
+
tenantId,
|
|
28983
|
+
workspaceId,
|
|
28984
|
+
id,
|
|
28985
|
+
"Observation",
|
|
28986
|
+
context,
|
|
28987
|
+
(existingResourceStr) => buildUpdatedResourceWithAudit(
|
|
28988
|
+
body,
|
|
28989
|
+
id,
|
|
28990
|
+
date,
|
|
28991
|
+
actorId,
|
|
28992
|
+
actorName,
|
|
28993
|
+
existingResourceStr,
|
|
28994
|
+
"Observation"
|
|
28995
|
+
)
|
|
28996
|
+
);
|
|
28997
|
+
}
|
|
28998
|
+
|
|
28999
|
+
// src/data/rest-api/routes/data/observation/observation-update-route.ts
|
|
29000
|
+
async function updateObservationRoute(req, res) {
|
|
29001
|
+
const bodyResult = requireJsonBodyAs(req, res);
|
|
29002
|
+
if ("errorResponse" in bodyResult) return bodyResult.errorResponse;
|
|
28936
29003
|
const id = String(req.params.id);
|
|
28937
29004
|
const ctx = req.openhiContext;
|
|
28938
29005
|
const body = bodyResult.body;
|
|
@@ -29835,492 +29902,109 @@ async function createPatientRoute(req, res) {
|
|
|
29835
29902
|
try {
|
|
29836
29903
|
const result = await createPatientOperation({
|
|
29837
29904
|
context: ctx,
|
|
29838
|
-
body: patient
|
|
29839
|
-
});
|
|
29840
|
-
return res.status(201).location(`${BASE_PATH.PATIENT}/${result.id}`).json(result.resource);
|
|
29841
|
-
} catch (err) {
|
|
29842
|
-
return sendOperationOutcome500(res, err, "POST Patient error:");
|
|
29843
|
-
}
|
|
29844
|
-
}
|
|
29845
|
-
|
|
29846
|
-
// src/data/operations/data/patient/patient-delete-operation.ts
|
|
29847
|
-
async function deletePatientOperation(params) {
|
|
29848
|
-
const { context, id, tableName } = params;
|
|
29849
|
-
const { tenantId, workspaceId } = context;
|
|
29850
|
-
const service = getDynamoDataService(tableName);
|
|
29851
|
-
await deleteDataEntityById(
|
|
29852
|
-
service.entities.patient,
|
|
29853
|
-
tenantId,
|
|
29854
|
-
workspaceId,
|
|
29855
|
-
id
|
|
29856
|
-
);
|
|
29857
|
-
}
|
|
29858
|
-
|
|
29859
|
-
// src/data/rest-api/routes/data/patient/patient-delete-route.ts
|
|
29860
|
-
async function deletePatientRoute(req, res) {
|
|
29861
|
-
const id = String(req.params.id);
|
|
29862
|
-
const ctx = req.openhiContext;
|
|
29863
|
-
try {
|
|
29864
|
-
await deletePatientOperation({ context: ctx, id });
|
|
29865
|
-
return res.status(204).send();
|
|
29866
|
-
} catch (err) {
|
|
29867
|
-
return sendOperationOutcome500(res, err, "DELETE Patient error:");
|
|
29868
|
-
}
|
|
29869
|
-
}
|
|
29870
|
-
|
|
29871
|
-
// src/data/operations/data/patient/patient-get-by-id-operation.ts
|
|
29872
|
-
async function getPatientByIdOperation(params) {
|
|
29873
|
-
const { context, id, tableName } = params;
|
|
29874
|
-
const { tenantId, workspaceId } = context;
|
|
29875
|
-
const service = getDynamoDataService(tableName);
|
|
29876
|
-
return getDataEntityById(
|
|
29877
|
-
service.entities.patient,
|
|
29878
|
-
tenantId,
|
|
29879
|
-
workspaceId,
|
|
29880
|
-
id,
|
|
29881
|
-
"Patient"
|
|
29882
|
-
);
|
|
29883
|
-
}
|
|
29884
|
-
|
|
29885
|
-
// src/data/rest-api/routes/data/patient/patient-get-by-id-route.ts
|
|
29886
|
-
async function getPatientByIdRoute(req, res) {
|
|
29887
|
-
const id = String(req.params.id);
|
|
29888
|
-
const ctx = req.openhiContext;
|
|
29889
|
-
try {
|
|
29890
|
-
const result = await getPatientByIdOperation({ context: ctx, id });
|
|
29891
|
-
return res.json(result.resource);
|
|
29892
|
-
} catch (err) {
|
|
29893
|
-
const status = domainErrorToHttpStatus(err);
|
|
29894
|
-
if (status === 404) {
|
|
29895
|
-
const diagnostics = err instanceof NotFoundError ? err.message : `Patient ${id} not found`;
|
|
29896
|
-
return sendOperationOutcome404(res, diagnostics);
|
|
29897
|
-
}
|
|
29898
|
-
return sendOperationOutcome500(res, err, "GET Patient error:");
|
|
29899
|
-
}
|
|
29900
|
-
}
|
|
29901
|
-
|
|
29902
|
-
// src/data/operations/data/patient/patient-list-operation.ts
|
|
29903
|
-
async function listPatientsOperation(params) {
|
|
29904
|
-
const { context, tableName, mode } = params;
|
|
29905
|
-
const { tenantId, workspaceId } = context;
|
|
29906
|
-
const service = getDynamoDataService(tableName);
|
|
29907
|
-
return listDataEntitiesByWorkspace(
|
|
29908
|
-
service.entities.patient,
|
|
29909
|
-
tenantId,
|
|
29910
|
-
workspaceId,
|
|
29911
|
-
mode
|
|
29912
|
-
);
|
|
29913
|
-
}
|
|
29914
|
-
|
|
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: [] };
|
|
29905
|
+
body: patient
|
|
29906
|
+
});
|
|
29907
|
+
return res.status(201).location(`${BASE_PATH.PATIENT}/${result.id}`).json(result.resource);
|
|
29908
|
+
} catch (err) {
|
|
29909
|
+
return sendOperationOutcome500(res, err, "POST Patient error:");
|
|
30203
29910
|
}
|
|
30204
|
-
return { sql: groupSqls.join(" AND "), params: allParams };
|
|
30205
29911
|
}
|
|
30206
29912
|
|
|
30207
|
-
// src/data/
|
|
30208
|
-
|
|
30209
|
-
|
|
30210
|
-
const
|
|
30211
|
-
|
|
30212
|
-
|
|
30213
|
-
|
|
30214
|
-
|
|
30215
|
-
|
|
30216
|
-
|
|
30217
|
-
|
|
30218
|
-
|
|
30219
|
-
|
|
29913
|
+
// src/data/operations/data/patient/patient-delete-operation.ts
|
|
29914
|
+
async function deletePatientOperation(params) {
|
|
29915
|
+
const { context, id, tableName } = params;
|
|
29916
|
+
const { tenantId, workspaceId } = context;
|
|
29917
|
+
const service = getDynamoDataService(tableName);
|
|
29918
|
+
await deleteDataEntityById(
|
|
29919
|
+
service.entities.patient,
|
|
29920
|
+
tenantId,
|
|
29921
|
+
workspaceId,
|
|
29922
|
+
id
|
|
29923
|
+
);
|
|
29924
|
+
}
|
|
29925
|
+
|
|
29926
|
+
// src/data/rest-api/routes/data/patient/patient-delete-route.ts
|
|
29927
|
+
async function deletePatientRoute(req, res) {
|
|
29928
|
+
const id = String(req.params.id);
|
|
29929
|
+
const ctx = req.openhiContext;
|
|
29930
|
+
try {
|
|
29931
|
+
await deletePatientOperation({ context: ctx, id });
|
|
29932
|
+
return res.status(204).send();
|
|
29933
|
+
} catch (err) {
|
|
29934
|
+
return sendOperationOutcome500(res, err, "DELETE Patient error:");
|
|
30220
29935
|
}
|
|
30221
|
-
lines.push("ORDER BY last_updated DESC");
|
|
30222
|
-
lines.push("LIMIT :limit;");
|
|
30223
|
-
return lines.join("\n");
|
|
30224
29936
|
}
|
|
30225
|
-
|
|
30226
|
-
|
|
30227
|
-
|
|
30228
|
-
const
|
|
30229
|
-
const
|
|
30230
|
-
const
|
|
30231
|
-
|
|
30232
|
-
|
|
30233
|
-
|
|
30234
|
-
|
|
30235
|
-
|
|
30236
|
-
|
|
30237
|
-
|
|
30238
|
-
{ name: "tenantId", value: tenantId },
|
|
30239
|
-
{ name: "workspaceId", value: workspaceId },
|
|
30240
|
-
{ name: "resourceType", value: resourceType },
|
|
30241
|
-
{ name: "limit", value: limit },
|
|
30242
|
-
...combined.params
|
|
30243
|
-
];
|
|
30244
|
-
const rows = await runner.query(sql, queryParams);
|
|
30245
|
-
const entries = rows.map((row) => ({
|
|
30246
|
-
id: row.id,
|
|
30247
|
-
resource: { ...row.resource, id: row.id }
|
|
30248
|
-
}));
|
|
30249
|
-
return { entries, total: entries.length };
|
|
29937
|
+
|
|
29938
|
+
// src/data/operations/data/patient/patient-get-by-id-operation.ts
|
|
29939
|
+
async function getPatientByIdOperation(params) {
|
|
29940
|
+
const { context, id, tableName } = params;
|
|
29941
|
+
const { tenantId, workspaceId } = context;
|
|
29942
|
+
const service = getDynamoDataService(tableName);
|
|
29943
|
+
return getDataEntityById(
|
|
29944
|
+
service.entities.patient,
|
|
29945
|
+
tenantId,
|
|
29946
|
+
workspaceId,
|
|
29947
|
+
id,
|
|
29948
|
+
"Patient"
|
|
29949
|
+
);
|
|
30250
29950
|
}
|
|
30251
29951
|
|
|
30252
|
-
// src/data/
|
|
30253
|
-
|
|
30254
|
-
|
|
30255
|
-
|
|
30256
|
-
{
|
|
30257
|
-
|
|
30258
|
-
|
|
30259
|
-
|
|
30260
|
-
|
|
30261
|
-
|
|
30262
|
-
|
|
30263
|
-
|
|
30264
|
-
|
|
30265
|
-
|
|
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"
|
|
29952
|
+
// src/data/rest-api/routes/data/patient/patient-get-by-id-route.ts
|
|
29953
|
+
async function getPatientByIdRoute(req, res) {
|
|
29954
|
+
const id = String(req.params.id);
|
|
29955
|
+
const ctx = req.openhiContext;
|
|
29956
|
+
try {
|
|
29957
|
+
const result = await getPatientByIdOperation({ context: ctx, id });
|
|
29958
|
+
return res.json(result.resource);
|
|
29959
|
+
} catch (err) {
|
|
29960
|
+
const status = domainErrorToHttpStatus(err);
|
|
29961
|
+
if (status === 404) {
|
|
29962
|
+
const diagnostics = err instanceof NotFoundError ? err.message : `Patient ${id} not found`;
|
|
29963
|
+
return sendOperationOutcome404(res, diagnostics);
|
|
29964
|
+
}
|
|
29965
|
+
return sendOperationOutcome500(res, err, "GET Patient error:");
|
|
30286
29966
|
}
|
|
30287
|
-
|
|
29967
|
+
}
|
|
30288
29968
|
|
|
30289
|
-
// src/data/
|
|
30290
|
-
|
|
30291
|
-
|
|
30292
|
-
};
|
|
30293
|
-
|
|
30294
|
-
|
|
30295
|
-
|
|
29969
|
+
// src/data/operations/data/patient/patient-list-operation.ts
|
|
29970
|
+
async function listPatientsOperation(params) {
|
|
29971
|
+
const { context, tableName, mode } = params;
|
|
29972
|
+
const { tenantId, workspaceId } = context;
|
|
29973
|
+
const service = getDynamoDataService(tableName);
|
|
29974
|
+
return listDataEntitiesByWorkspace(
|
|
29975
|
+
service.entities.patient,
|
|
29976
|
+
tenantId,
|
|
29977
|
+
workspaceId,
|
|
29978
|
+
mode
|
|
29979
|
+
);
|
|
30296
29980
|
}
|
|
30297
29981
|
|
|
30298
29982
|
// src/data/rest-api/routes/data/patient/patient-list-route.ts
|
|
30299
29983
|
var PATIENT_RESOURCE_TYPE = "Patient";
|
|
30300
|
-
function
|
|
29984
|
+
function stripModifier4(key) {
|
|
30301
29985
|
const idx = key.indexOf(":");
|
|
30302
29986
|
return idx === -1 ? key : key.slice(0, idx);
|
|
30303
29987
|
}
|
|
30304
|
-
function
|
|
29988
|
+
function isResultParameter4(key) {
|
|
30305
29989
|
return key.startsWith("_");
|
|
30306
29990
|
}
|
|
30307
|
-
function
|
|
29991
|
+
function sendInvalidSearch4004(res, diagnostics) {
|
|
30308
29992
|
return res.status(400).json({
|
|
30309
29993
|
resourceType: "OperationOutcome",
|
|
30310
29994
|
issue: [{ severity: "error", code: "invalid", diagnostics }]
|
|
30311
29995
|
});
|
|
30312
29996
|
}
|
|
30313
|
-
function
|
|
29997
|
+
function extractSearchParamKeys4(query) {
|
|
30314
29998
|
const out = [];
|
|
30315
29999
|
for (const rawKey of Object.keys(query)) {
|
|
30316
|
-
if (
|
|
30000
|
+
if (isResultParameter4(rawKey)) {
|
|
30317
30001
|
continue;
|
|
30318
30002
|
}
|
|
30319
|
-
out.push({ rawKey, code:
|
|
30003
|
+
out.push({ rawKey, code: stripModifier4(rawKey) });
|
|
30320
30004
|
}
|
|
30321
30005
|
return out;
|
|
30322
30006
|
}
|
|
30323
|
-
function
|
|
30007
|
+
function buildUnknownParamDiagnostics4(unknownCodes) {
|
|
30324
30008
|
const validCodes = getRegisteredSearchParameters(PATIENT_RESOURCE_TYPE).map((p) => p.code).sort().join(", ");
|
|
30325
30009
|
const codes = unknownCodes.join(", ");
|
|
30326
30010
|
const isPlural = unknownCodes.length !== 1;
|
|
@@ -30329,7 +30013,7 @@ function buildUnknownParamDiagnostics(unknownCodes) {
|
|
|
30329
30013
|
`Valid codes: ${validCodes}.`
|
|
30330
30014
|
].join(" ");
|
|
30331
30015
|
}
|
|
30332
|
-
function
|
|
30016
|
+
function findMalformedReference4(query, searchParamKeys) {
|
|
30333
30017
|
const referenceCodes = new Set(
|
|
30334
30018
|
getRegisteredSearchParameters(PATIENT_RESOURCE_TYPE).filter((p) => p.type === "reference").map((p) => p.code)
|
|
30335
30019
|
);
|
|
@@ -30352,7 +30036,7 @@ function findMalformedReference(query, searchParamKeys) {
|
|
|
30352
30036
|
return void 0;
|
|
30353
30037
|
}
|
|
30354
30038
|
async function listPatientsRoute(req, res) {
|
|
30355
|
-
const searchParamKeys =
|
|
30039
|
+
const searchParamKeys = extractSearchParamKeys4(
|
|
30356
30040
|
req.query
|
|
30357
30041
|
);
|
|
30358
30042
|
if (searchParamKeys.length === 0) {
|
|
@@ -30368,17 +30052,17 @@ async function listPatientsRoute(req, res) {
|
|
|
30368
30052
|
const validCodes = new Set(registered.map((p) => p.code));
|
|
30369
30053
|
const unknownCodes = searchParamKeys.map((k) => k.code).filter((code) => !validCodes.has(code));
|
|
30370
30054
|
if (unknownCodes.length > 0) {
|
|
30371
|
-
return
|
|
30055
|
+
return sendInvalidSearch4004(
|
|
30372
30056
|
res,
|
|
30373
|
-
|
|
30057
|
+
buildUnknownParamDiagnostics4([...new Set(unknownCodes)])
|
|
30374
30058
|
);
|
|
30375
30059
|
}
|
|
30376
|
-
const malformedRef =
|
|
30060
|
+
const malformedRef = findMalformedReference4(
|
|
30377
30061
|
req.query,
|
|
30378
30062
|
searchParamKeys
|
|
30379
30063
|
);
|
|
30380
30064
|
if (malformedRef !== void 0) {
|
|
30381
|
-
return
|
|
30065
|
+
return sendInvalidSearch4004(
|
|
30382
30066
|
res,
|
|
30383
30067
|
`?${malformedRef.rawKey} must be a typed reference like "Practitioner/<id>"; got "${malformedRef.value}".`
|
|
30384
30068
|
);
|
|
@@ -31752,15 +31436,108 @@ async function listProceduresOperation(params) {
|
|
|
31752
31436
|
}
|
|
31753
31437
|
|
|
31754
31438
|
// src/data/rest-api/routes/data/procedure/procedure-list-route.ts
|
|
31755
|
-
|
|
31756
|
-
|
|
31757
|
-
|
|
31758
|
-
|
|
31759
|
-
|
|
31760
|
-
|
|
31761
|
-
|
|
31439
|
+
var PROCEDURE_RESOURCE_TYPE = "Procedure";
|
|
31440
|
+
function stripModifier5(key) {
|
|
31441
|
+
const idx = key.indexOf(":");
|
|
31442
|
+
return idx === -1 ? key : key.slice(0, idx);
|
|
31443
|
+
}
|
|
31444
|
+
function isResultParameter5(key) {
|
|
31445
|
+
return key.startsWith("_");
|
|
31446
|
+
}
|
|
31447
|
+
function sendInvalidSearch4005(res, diagnostics) {
|
|
31448
|
+
return res.status(400).json({
|
|
31449
|
+
resourceType: "OperationOutcome",
|
|
31450
|
+
issue: [{ severity: "error", code: "invalid", diagnostics }]
|
|
31762
31451
|
});
|
|
31763
31452
|
}
|
|
31453
|
+
function extractSearchParamKeys5(query) {
|
|
31454
|
+
const out = [];
|
|
31455
|
+
for (const rawKey of Object.keys(query)) {
|
|
31456
|
+
if (isResultParameter5(rawKey)) {
|
|
31457
|
+
continue;
|
|
31458
|
+
}
|
|
31459
|
+
out.push({ rawKey, code: stripModifier5(rawKey) });
|
|
31460
|
+
}
|
|
31461
|
+
return out;
|
|
31462
|
+
}
|
|
31463
|
+
function buildUnknownParamDiagnostics5(unknownCodes) {
|
|
31464
|
+
const validCodes = getRegisteredSearchParameters(PROCEDURE_RESOURCE_TYPE).map((p) => p.code).sort().join(", ");
|
|
31465
|
+
const codes = unknownCodes.join(", ");
|
|
31466
|
+
const isPlural = unknownCodes.length !== 1;
|
|
31467
|
+
return [
|
|
31468
|
+
`Unknown search ${isPlural ? "parameters" : "parameter"} for Procedure: ${codes}.`,
|
|
31469
|
+
`Valid codes: ${validCodes}.`
|
|
31470
|
+
].join(" ");
|
|
31471
|
+
}
|
|
31472
|
+
function findMalformedReference5(query, searchParamKeys) {
|
|
31473
|
+
const referenceCodes = new Set(
|
|
31474
|
+
getRegisteredSearchParameters(PROCEDURE_RESOURCE_TYPE).filter((p) => p.type === "reference").map((p) => p.code)
|
|
31475
|
+
);
|
|
31476
|
+
for (const { rawKey, code } of searchParamKeys) {
|
|
31477
|
+
if (!referenceCodes.has(code)) {
|
|
31478
|
+
continue;
|
|
31479
|
+
}
|
|
31480
|
+
const raw = query[rawKey];
|
|
31481
|
+
const values = typeof raw === "string" ? raw.split(",") : Array.isArray(raw) ? raw.flatMap((v) => v.split(",")) : [];
|
|
31482
|
+
for (const v of values) {
|
|
31483
|
+
const trimmed = v.trim();
|
|
31484
|
+
if (trimmed.length === 0) {
|
|
31485
|
+
continue;
|
|
31486
|
+
}
|
|
31487
|
+
if (parseTypedReference(trimmed) === void 0) {
|
|
31488
|
+
return { rawKey, value: trimmed };
|
|
31489
|
+
}
|
|
31490
|
+
}
|
|
31491
|
+
}
|
|
31492
|
+
return void 0;
|
|
31493
|
+
}
|
|
31494
|
+
async function listProceduresRoute(req, res) {
|
|
31495
|
+
const searchParamKeys = extractSearchParamKeys5(
|
|
31496
|
+
req.query
|
|
31497
|
+
);
|
|
31498
|
+
if (searchParamKeys.length === 0) {
|
|
31499
|
+
return handleListRoute({
|
|
31500
|
+
req,
|
|
31501
|
+
res,
|
|
31502
|
+
basePath: BASE_PATH.PROCEDURE,
|
|
31503
|
+
listOperation: listProceduresOperation,
|
|
31504
|
+
errorLogContext: "GET /Procedure list error:"
|
|
31505
|
+
});
|
|
31506
|
+
}
|
|
31507
|
+
const registered = getRegisteredSearchParameters(PROCEDURE_RESOURCE_TYPE);
|
|
31508
|
+
const validCodes = new Set(registered.map((p) => p.code));
|
|
31509
|
+
const unknownCodes = searchParamKeys.map((k) => k.code).filter((code) => !validCodes.has(code));
|
|
31510
|
+
if (unknownCodes.length > 0) {
|
|
31511
|
+
return sendInvalidSearch4005(
|
|
31512
|
+
res,
|
|
31513
|
+
buildUnknownParamDiagnostics5([...new Set(unknownCodes)])
|
|
31514
|
+
);
|
|
31515
|
+
}
|
|
31516
|
+
const malformedRef = findMalformedReference5(
|
|
31517
|
+
req.query,
|
|
31518
|
+
searchParamKeys
|
|
31519
|
+
);
|
|
31520
|
+
if (malformedRef !== void 0) {
|
|
31521
|
+
return sendInvalidSearch4005(
|
|
31522
|
+
res,
|
|
31523
|
+
`?${malformedRef.rawKey} must be a typed reference like "Practitioner/<id>"; got "${malformedRef.value}".`
|
|
31524
|
+
);
|
|
31525
|
+
}
|
|
31526
|
+
const ctx = req.openhiContext;
|
|
31527
|
+
try {
|
|
31528
|
+
const result = await genericSearchOperation({
|
|
31529
|
+
resourceType: PROCEDURE_RESOURCE_TYPE,
|
|
31530
|
+
tenantId: ctx.tenantId,
|
|
31531
|
+
workspaceId: ctx.workspaceId,
|
|
31532
|
+
query: req.query,
|
|
31533
|
+
resolver: defaultSearchParameterResolver
|
|
31534
|
+
});
|
|
31535
|
+
const bundle = buildSearchsetBundle(BASE_PATH.PROCEDURE, result.entries);
|
|
31536
|
+
return res.json(bundle);
|
|
31537
|
+
} catch (err) {
|
|
31538
|
+
return sendOperationOutcome500(res, err, "GET /Procedure search error:");
|
|
31539
|
+
}
|
|
31540
|
+
}
|
|
31764
31541
|
|
|
31765
31542
|
// src/data/operations/data/procedure/procedure-update-operation.ts
|
|
31766
31543
|
async function updateProcedureOperation(params) {
|
|
@@ -34216,7 +33993,7 @@ async function listSchedulesOperation(params) {
|
|
|
34216
33993
|
}
|
|
34217
33994
|
|
|
34218
33995
|
// src/data/operations/data/schedule/schedule-search-by-actor-operation.ts
|
|
34219
|
-
var
|
|
33996
|
+
var DEFAULT_LIMIT2 = 100;
|
|
34220
33997
|
function buildSearchSchedulesByActorSql() {
|
|
34221
33998
|
return [
|
|
34222
33999
|
"SELECT resource_id AS id, resource",
|
|
@@ -34234,7 +34011,7 @@ async function searchSchedulesByActorOperation(params) {
|
|
|
34234
34011
|
const { context, actorReference } = params;
|
|
34235
34012
|
const { tenantId, workspaceId } = context;
|
|
34236
34013
|
const runner = params.runner ?? getDefaultPostgresQueryRunner();
|
|
34237
|
-
const limit = params.limit ??
|
|
34014
|
+
const limit = params.limit ?? DEFAULT_LIMIT2;
|
|
34238
34015
|
const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
|
|
34239
34016
|
shape: { kind: "array-of-references", field: "actor" },
|
|
34240
34017
|
reference: actorReference,
|
|
@@ -34260,7 +34037,7 @@ async function searchSchedulesByActorOperation(params) {
|
|
|
34260
34037
|
}
|
|
34261
34038
|
|
|
34262
34039
|
// src/data/rest-api/routes/data/schedule/schedule-list-route.ts
|
|
34263
|
-
function
|
|
34040
|
+
function singleStringQueryParam(req, name) {
|
|
34264
34041
|
const v = req.query[name];
|
|
34265
34042
|
if (typeof v !== "string") {
|
|
34266
34043
|
return void 0;
|
|
@@ -34268,17 +34045,17 @@ function singleStringQueryParam3(req, name) {
|
|
|
34268
34045
|
const trimmed = v.trim();
|
|
34269
34046
|
return trimmed === "" ? void 0 : trimmed;
|
|
34270
34047
|
}
|
|
34271
|
-
function
|
|
34048
|
+
function sendInvalidSearch4006(res, diagnostics) {
|
|
34272
34049
|
return res.status(400).json({
|
|
34273
34050
|
resourceType: "OperationOutcome",
|
|
34274
34051
|
issue: [{ severity: "error", code: "invalid", diagnostics }]
|
|
34275
34052
|
});
|
|
34276
34053
|
}
|
|
34277
34054
|
async function listSchedulesRoute(req, res) {
|
|
34278
|
-
const actorRef =
|
|
34055
|
+
const actorRef = singleStringQueryParam(req, "actor");
|
|
34279
34056
|
if (actorRef !== void 0) {
|
|
34280
34057
|
if (parseTypedReference(actorRef) === void 0) {
|
|
34281
|
-
return
|
|
34058
|
+
return sendInvalidSearch4006(
|
|
34282
34059
|
res,
|
|
34283
34060
|
`?actor must be a typed reference like "Practitioner/<id>"; got "${actorRef}".`
|
|
34284
34061
|
);
|
|
@@ -37980,7 +37757,7 @@ async function listTasksOperation(params) {
|
|
|
37980
37757
|
}
|
|
37981
37758
|
|
|
37982
37759
|
// src/data/operations/data/task/task-search-by-owner-operation.ts
|
|
37983
|
-
var
|
|
37760
|
+
var DEFAULT_LIMIT3 = 100;
|
|
37984
37761
|
function buildSearchTasksByOwnerSql() {
|
|
37985
37762
|
return [
|
|
37986
37763
|
"SELECT resource_id AS id, resource",
|
|
@@ -37998,7 +37775,7 @@ async function searchTasksByOwnerOperation(params) {
|
|
|
37998
37775
|
const { context, ownerReference } = params;
|
|
37999
37776
|
const { tenantId, workspaceId } = context;
|
|
38000
37777
|
const runner = params.runner ?? getDefaultPostgresQueryRunner();
|
|
38001
|
-
const limit = params.limit ??
|
|
37778
|
+
const limit = params.limit ?? DEFAULT_LIMIT3;
|
|
38002
37779
|
const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
|
|
38003
37780
|
shape: { kind: "scalar", field: "owner" },
|
|
38004
37781
|
reference: ownerReference,
|
|
@@ -38024,7 +37801,7 @@ async function searchTasksByOwnerOperation(params) {
|
|
|
38024
37801
|
}
|
|
38025
37802
|
|
|
38026
37803
|
// src/data/operations/data/task/task-search-by-requester-operation.ts
|
|
38027
|
-
var
|
|
37804
|
+
var DEFAULT_LIMIT4 = 100;
|
|
38028
37805
|
function buildSearchTasksByRequesterSql() {
|
|
38029
37806
|
return [
|
|
38030
37807
|
"SELECT resource_id AS id, resource",
|
|
@@ -38042,7 +37819,7 @@ async function searchTasksByRequesterOperation(params) {
|
|
|
38042
37819
|
const { context, requesterReference } = params;
|
|
38043
37820
|
const { tenantId, workspaceId } = context;
|
|
38044
37821
|
const runner = params.runner ?? getDefaultPostgresQueryRunner();
|
|
38045
|
-
const limit = params.limit ??
|
|
37822
|
+
const limit = params.limit ?? DEFAULT_LIMIT4;
|
|
38046
37823
|
const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
|
|
38047
37824
|
shape: { kind: "scalar", field: "requester" },
|
|
38048
37825
|
reference: requesterReference,
|
|
@@ -38068,7 +37845,7 @@ async function searchTasksByRequesterOperation(params) {
|
|
|
38068
37845
|
}
|
|
38069
37846
|
|
|
38070
37847
|
// src/data/rest-api/routes/data/task/task-list-route.ts
|
|
38071
|
-
function
|
|
37848
|
+
function singleStringQueryParam2(req, name) {
|
|
38072
37849
|
const v = req.query[name];
|
|
38073
37850
|
if (typeof v !== "string") {
|
|
38074
37851
|
return void 0;
|
|
@@ -38076,24 +37853,24 @@ function singleStringQueryParam4(req, name) {
|
|
|
38076
37853
|
const trimmed = v.trim();
|
|
38077
37854
|
return trimmed === "" ? void 0 : trimmed;
|
|
38078
37855
|
}
|
|
38079
|
-
function
|
|
37856
|
+
function sendInvalidSearch4007(res, diagnostics) {
|
|
38080
37857
|
return res.status(400).json({
|
|
38081
37858
|
resourceType: "OperationOutcome",
|
|
38082
37859
|
issue: [{ severity: "error", code: "invalid", diagnostics }]
|
|
38083
37860
|
});
|
|
38084
37861
|
}
|
|
38085
37862
|
async function listTasksRoute(req, res) {
|
|
38086
|
-
const ownerRef =
|
|
38087
|
-
const requesterRef =
|
|
37863
|
+
const ownerRef = singleStringQueryParam2(req, "owner");
|
|
37864
|
+
const requesterRef = singleStringQueryParam2(req, "requester");
|
|
38088
37865
|
if (ownerRef !== void 0 && requesterRef !== void 0) {
|
|
38089
|
-
return
|
|
37866
|
+
return sendInvalidSearch4007(
|
|
38090
37867
|
res,
|
|
38091
37868
|
"?owner= and ?requester= cannot be combined on the same request."
|
|
38092
37869
|
);
|
|
38093
37870
|
}
|
|
38094
37871
|
if (ownerRef !== void 0) {
|
|
38095
37872
|
if (parseTypedReference(ownerRef) === void 0) {
|
|
38096
|
-
return
|
|
37873
|
+
return sendInvalidSearch4007(
|
|
38097
37874
|
res,
|
|
38098
37875
|
`?owner must be a typed reference like "Practitioner/<id>"; got "${ownerRef}".`
|
|
38099
37876
|
);
|
|
@@ -38116,7 +37893,7 @@ async function listTasksRoute(req, res) {
|
|
|
38116
37893
|
}
|
|
38117
37894
|
if (requesterRef !== void 0) {
|
|
38118
37895
|
if (parseTypedReference(requesterRef) === void 0) {
|
|
38119
|
-
return
|
|
37896
|
+
return sendInvalidSearch4007(
|
|
38120
37897
|
res,
|
|
38121
37898
|
`?requester must be a typed reference like "Practitioner/<id>"; got "${requesterRef}".`
|
|
38122
37899
|
);
|