@openhi/constructs 0.0.138 → 0.0.140
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.
|
@@ -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
|
|
|
@@ -9827,129 +9910,95 @@ function emitFieldMissingPredicate(opts) {
|
|
|
9827
9910
|
const sql = opts.missing ? `${extract} IS NULL` : `${extract} IS NOT NULL`;
|
|
9828
9911
|
return { sql, params: [] };
|
|
9829
9912
|
}
|
|
9830
|
-
|
|
9831
|
-
|
|
9832
|
-
|
|
9913
|
+
|
|
9914
|
+
// src/data/search/engine/string-predicate.ts
|
|
9915
|
+
var STRING_MODIFIERS = ["exact", "contains"];
|
|
9916
|
+
function isStringModifier(s) {
|
|
9917
|
+
return STRING_MODIFIERS.includes(s);
|
|
9833
9918
|
}
|
|
9834
|
-
function
|
|
9835
|
-
|
|
9836
|
-
|
|
9837
|
-
|
|
9838
|
-
case "gt":
|
|
9839
|
-
return `(${endExtract} IS NULL OR ${endExtract} > :${paramName})`;
|
|
9840
|
-
case "lt":
|
|
9841
|
-
return `(${startExtract} IS NULL OR ${startExtract} < :${paramName})`;
|
|
9842
|
-
case "ge":
|
|
9843
|
-
return `(${endExtract} IS NULL OR ${endExtract} >= :${paramName})`;
|
|
9844
|
-
case "le":
|
|
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})`;
|
|
9919
|
+
function jsonbPathToStringShape(jsonbPath) {
|
|
9920
|
+
const scalar = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
|
|
9921
|
+
if (scalar) {
|
|
9922
|
+
return { kind: "scalar", field: scalar[1] };
|
|
9850
9923
|
}
|
|
9851
|
-
|
|
9852
|
-
|
|
9853
|
-
|
|
9854
|
-
return [];
|
|
9924
|
+
const arrayOfScalars = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
|
|
9925
|
+
if (arrayOfScalars) {
|
|
9926
|
+
return { kind: "array-of-scalars", field: arrayOfScalars[1] };
|
|
9855
9927
|
}
|
|
9856
|
-
const
|
|
9857
|
-
|
|
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
|
-
)
|
|
9928
|
+
const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
|
|
9929
|
+
jsonbPath
|
|
9866
9930
|
);
|
|
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
|
|
9931
|
+
if (arrayOfObjs) {
|
|
9932
|
+
return {
|
|
9933
|
+
kind: "array-of-objects",
|
|
9934
|
+
field: arrayOfObjs[1],
|
|
9935
|
+
subfield: arrayOfObjs[2]
|
|
9936
|
+
};
|
|
9937
|
+
}
|
|
9938
|
+
throw new Error(
|
|
9939
|
+
`String predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
|
|
9888
9940
|
);
|
|
9889
9941
|
}
|
|
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}`;
|
|
9942
|
+
function escapeLikePattern(value) {
|
|
9943
|
+
return value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
9906
9944
|
}
|
|
9907
|
-
|
|
9908
|
-
|
|
9909
|
-
|
|
9910
|
-
|
|
9911
|
-
|
|
9945
|
+
function buildLikePattern(value, modifier) {
|
|
9946
|
+
const escaped = escapeLikePattern(value);
|
|
9947
|
+
switch (modifier) {
|
|
9948
|
+
case "exact":
|
|
9949
|
+
return escaped;
|
|
9950
|
+
case "contains":
|
|
9951
|
+
return `%${escaped}%`;
|
|
9952
|
+
case void 0:
|
|
9953
|
+
return `${escaped}%`;
|
|
9912
9954
|
}
|
|
9913
|
-
return { resourceType: match[1], resourceId: match[2] };
|
|
9914
9955
|
}
|
|
9915
|
-
function
|
|
9956
|
+
function buildIlikeExtractSql(shape, paramName) {
|
|
9916
9957
|
switch (shape.kind) {
|
|
9917
9958
|
case "scalar":
|
|
9918
|
-
return {
|
|
9919
|
-
case "array-of-
|
|
9920
|
-
return
|
|
9959
|
+
return `resource->>'${shape.field}' ILIKE :${paramName}`;
|
|
9960
|
+
case "array-of-scalars":
|
|
9961
|
+
return [
|
|
9962
|
+
"EXISTS (SELECT 1 FROM",
|
|
9963
|
+
`jsonb_array_elements_text(resource->'${shape.field}') AS s_elem(text_val)`,
|
|
9964
|
+
`WHERE s_elem.text_val ILIKE :${paramName})`
|
|
9965
|
+
].join(" ");
|
|
9921
9966
|
case "array-of-objects":
|
|
9922
|
-
return
|
|
9967
|
+
return [
|
|
9968
|
+
"EXISTS (SELECT 1 FROM",
|
|
9969
|
+
`jsonb_array_elements(resource->'${shape.field}') AS s_obj(obj)`,
|
|
9970
|
+
`WHERE s_obj.obj->>'${shape.subfield}' ILIKE :${paramName})`
|
|
9971
|
+
].join(" ");
|
|
9923
9972
|
}
|
|
9924
9973
|
}
|
|
9925
|
-
function
|
|
9926
|
-
const
|
|
9927
|
-
|
|
9974
|
+
function emitStringPredicate(opts) {
|
|
9975
|
+
const shape = jsonbPathToStringShape(opts.jsonbPath);
|
|
9976
|
+
const modifier = opts.modifier === void 0 ? void 0 : checkModifier(opts.modifier);
|
|
9977
|
+
const sql = buildIlikeExtractSql(shape, opts.paramName);
|
|
9978
|
+
const pattern = buildLikePattern(opts.rawValue, modifier);
|
|
9979
|
+
const params = [
|
|
9980
|
+
{ name: opts.paramName, value: pattern }
|
|
9981
|
+
];
|
|
9982
|
+
return { sql, params };
|
|
9983
|
+
}
|
|
9984
|
+
function checkModifier(modifier) {
|
|
9985
|
+
if (!isStringModifier(modifier)) {
|
|
9928
9986
|
throw new Error(
|
|
9929
|
-
`
|
|
9987
|
+
`String predicate does not support modifier ":${modifier}". Supported: ${STRING_MODIFIERS.map((m) => `:${m}`).join(", ")}.`
|
|
9930
9988
|
);
|
|
9931
9989
|
}
|
|
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
|
-
};
|
|
9990
|
+
return modifier;
|
|
9944
9991
|
}
|
|
9945
|
-
|
|
9992
|
+
|
|
9993
|
+
// src/data/search/engine/token-predicate.ts
|
|
9994
|
+
function jsonbPathToTokenShape(jsonbPath) {
|
|
9946
9995
|
const scalar = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
|
|
9947
9996
|
if (scalar) {
|
|
9948
9997
|
return { kind: "scalar", field: scalar[1] };
|
|
9949
9998
|
}
|
|
9950
|
-
const
|
|
9951
|
-
if (
|
|
9952
|
-
return { kind: "array-of-
|
|
9999
|
+
const arrayOfScalars = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
|
|
10000
|
+
if (arrayOfScalars) {
|
|
10001
|
+
return { kind: "array-of-scalars", field: arrayOfScalars[1] };
|
|
9953
10002
|
}
|
|
9954
10003
|
const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
|
|
9955
10004
|
jsonbPath
|
|
@@ -9962,240 +10011,332 @@ function jsonbPathToReferenceShape(jsonbPath) {
|
|
|
9962
10011
|
};
|
|
9963
10012
|
}
|
|
9964
10013
|
throw new Error(
|
|
9965
|
-
`
|
|
10014
|
+
`Token predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
|
|
9966
10015
|
);
|
|
9967
10016
|
}
|
|
9968
|
-
function
|
|
9969
|
-
const
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
}
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
10017
|
+
function parseTokenValue(raw) {
|
|
10018
|
+
const idx = raw.indexOf("|");
|
|
10019
|
+
if (idx === -1) {
|
|
10020
|
+
return { code: raw };
|
|
10021
|
+
}
|
|
10022
|
+
const system = raw.slice(0, idx);
|
|
10023
|
+
const code = raw.slice(idx + 1);
|
|
10024
|
+
return system.length > 0 ? { system, code } : { code };
|
|
10025
|
+
}
|
|
10026
|
+
function wrapTokenInShape(shape, value) {
|
|
10027
|
+
switch (shape.kind) {
|
|
10028
|
+
case "scalar":
|
|
10029
|
+
return { [shape.field]: value };
|
|
10030
|
+
case "array-of-scalars":
|
|
10031
|
+
return { [shape.field]: [value] };
|
|
10032
|
+
case "array-of-objects":
|
|
10033
|
+
return { [shape.field]: [{ [shape.subfield]: value }] };
|
|
10034
|
+
}
|
|
10035
|
+
}
|
|
10036
|
+
function emitTokenPredicate(opts) {
|
|
10037
|
+
const shape = jsonbPathToTokenShape(opts.jsonbPath);
|
|
10038
|
+
const { code } = parseTokenValue(opts.rawValue);
|
|
10039
|
+
const payload = wrapTokenInShape(shape, code);
|
|
10040
|
+
const sql = `resource @> :${opts.paramName}::jsonb`;
|
|
9979
10041
|
const params = [
|
|
9980
|
-
{ name:
|
|
9981
|
-
{ name: urnName, value: containmentUrn }
|
|
10042
|
+
{ name: opts.paramName, value: JSON.stringify(payload) }
|
|
9982
10043
|
];
|
|
9983
10044
|
return { sql, params };
|
|
9984
10045
|
}
|
|
9985
10046
|
|
|
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}`);
|
|
10047
|
+
// src/data/search/engine/combinator.ts
|
|
10048
|
+
function parseQueryKey(key) {
|
|
10049
|
+
const idx = key.indexOf(":");
|
|
10050
|
+
if (idx === -1) {
|
|
10051
|
+
return { code: key, modifier: void 0 };
|
|
10003
10052
|
}
|
|
10004
|
-
|
|
10005
|
-
lines.push("LIMIT :limit;");
|
|
10006
|
-
return lines.join("\n");
|
|
10053
|
+
return { code: key.slice(0, idx), modifier: key.slice(idx + 1) };
|
|
10007
10054
|
}
|
|
10008
|
-
|
|
10009
|
-
const
|
|
10010
|
-
const
|
|
10011
|
-
const
|
|
10012
|
-
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
|
|
10016
|
-
|
|
10017
|
-
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
10021
|
-
|
|
10022
|
-
|
|
10055
|
+
function flattenQueryValues(raw) {
|
|
10056
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
10057
|
+
const out = [];
|
|
10058
|
+
for (const v of list) {
|
|
10059
|
+
for (const piece of v.split(",")) {
|
|
10060
|
+
out.push(piece);
|
|
10061
|
+
}
|
|
10062
|
+
}
|
|
10063
|
+
return out;
|
|
10064
|
+
}
|
|
10065
|
+
function parseQueryEntries(query) {
|
|
10066
|
+
const entries = [];
|
|
10067
|
+
for (const [rawKey, rawValue] of Object.entries(query)) {
|
|
10068
|
+
if (rawValue === void 0) continue;
|
|
10069
|
+
const { code, modifier } = parseQueryKey(rawKey);
|
|
10070
|
+
entries.push({
|
|
10071
|
+
code,
|
|
10072
|
+
modifier,
|
|
10073
|
+
values: flattenQueryValues(rawValue)
|
|
10074
|
+
});
|
|
10075
|
+
}
|
|
10076
|
+
return entries;
|
|
10077
|
+
}
|
|
10078
|
+
function findParam(params, code) {
|
|
10079
|
+
return params.find((p) => p.code === code);
|
|
10080
|
+
}
|
|
10081
|
+
function checkModifierAllowed(modifier, param) {
|
|
10082
|
+
const universal = modifier === "missing" || modifier === "not";
|
|
10083
|
+
const stringNative = param.type === "string" && (modifier === "exact" || modifier === "contains");
|
|
10084
|
+
if (universal || stringNative) {
|
|
10085
|
+
if (param.modifiers && !param.modifiers.includes(modifier)) {
|
|
10086
|
+
throw new Error(
|
|
10087
|
+
`Modifier ":${modifier}" is not in the allow-list for param "${param.code}".`
|
|
10088
|
+
);
|
|
10089
|
+
}
|
|
10090
|
+
return;
|
|
10091
|
+
}
|
|
10092
|
+
throw new Error(
|
|
10093
|
+
`Modifier ":${modifier}" is not recognized for param "${param.code}" (type "${param.type}").`
|
|
10094
|
+
);
|
|
10095
|
+
}
|
|
10096
|
+
function emitOne(opts) {
|
|
10097
|
+
const { param, modifier, rawValue, paramName, context } = opts;
|
|
10098
|
+
if (modifier === "missing") {
|
|
10099
|
+
const missing = parseMissingValue(rawValue);
|
|
10100
|
+
return emitFieldMissingPredicate({
|
|
10101
|
+
jsonbPath: param.jsonbPath,
|
|
10102
|
+
missing
|
|
10103
|
+
});
|
|
10104
|
+
}
|
|
10105
|
+
const negate = modifier === "not";
|
|
10106
|
+
const effectiveModifier = negate ? void 0 : modifier;
|
|
10107
|
+
const inner = emitForType({
|
|
10108
|
+
paramType: param.type,
|
|
10109
|
+
jsonbPath: param.jsonbPath,
|
|
10110
|
+
rawValue,
|
|
10111
|
+
paramName,
|
|
10112
|
+
modifier: effectiveModifier,
|
|
10113
|
+
context
|
|
10023
10114
|
});
|
|
10024
|
-
|
|
10025
|
-
|
|
10026
|
-
|
|
10027
|
-
|
|
10028
|
-
|
|
10029
|
-
|
|
10030
|
-
|
|
10031
|
-
|
|
10032
|
-
|
|
10033
|
-
|
|
10034
|
-
|
|
10035
|
-
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
10115
|
+
if (!negate) {
|
|
10116
|
+
return inner;
|
|
10117
|
+
}
|
|
10118
|
+
return { sql: `NOT (${inner.sql})`, params: inner.params };
|
|
10119
|
+
}
|
|
10120
|
+
function parseMissingValue(raw) {
|
|
10121
|
+
if (raw === "true") return true;
|
|
10122
|
+
if (raw === "false") return false;
|
|
10123
|
+
throw new Error(
|
|
10124
|
+
`:missing requires a value of "true" or "false"; received "${raw}".`
|
|
10125
|
+
);
|
|
10126
|
+
}
|
|
10127
|
+
function emitForType(opts) {
|
|
10128
|
+
switch (opts.paramType) {
|
|
10129
|
+
case "token":
|
|
10130
|
+
return emitTokenPredicate({
|
|
10131
|
+
jsonbPath: opts.jsonbPath,
|
|
10132
|
+
rawValue: opts.rawValue,
|
|
10133
|
+
paramName: opts.paramName,
|
|
10134
|
+
context: opts.context
|
|
10135
|
+
});
|
|
10136
|
+
case "date":
|
|
10137
|
+
return emitDatePredicate({
|
|
10138
|
+
jsonbPath: opts.jsonbPath,
|
|
10139
|
+
rawValue: opts.rawValue,
|
|
10140
|
+
paramName: opts.paramName,
|
|
10141
|
+
context: opts.context
|
|
10142
|
+
});
|
|
10143
|
+
case "reference":
|
|
10144
|
+
return emitReferencePredicate({
|
|
10145
|
+
jsonbPath: opts.jsonbPath,
|
|
10146
|
+
rawValue: opts.rawValue,
|
|
10147
|
+
paramName: opts.paramName,
|
|
10148
|
+
context: opts.context
|
|
10149
|
+
});
|
|
10150
|
+
case "string":
|
|
10151
|
+
return emitStringPredicate({
|
|
10152
|
+
jsonbPath: opts.jsonbPath,
|
|
10153
|
+
rawValue: opts.rawValue,
|
|
10154
|
+
paramName: opts.paramName,
|
|
10155
|
+
modifier: opts.modifier,
|
|
10156
|
+
context: opts.context
|
|
10157
|
+
});
|
|
10158
|
+
}
|
|
10159
|
+
}
|
|
10160
|
+
function combineSearchPredicates(opts) {
|
|
10161
|
+
const entries = parseQueryEntries(opts.query);
|
|
10162
|
+
const groupSqls = [];
|
|
10163
|
+
const allParams = [];
|
|
10164
|
+
let paramIdx = 0;
|
|
10165
|
+
for (const entry of entries) {
|
|
10166
|
+
const registered = findParam(opts.registeredParams, entry.code);
|
|
10167
|
+
if (!registered) {
|
|
10168
|
+
continue;
|
|
10039
10169
|
}
|
|
10040
|
-
|
|
10041
|
-
|
|
10170
|
+
if (entry.modifier !== void 0) {
|
|
10171
|
+
checkModifierAllowed(entry.modifier, registered);
|
|
10172
|
+
}
|
|
10173
|
+
if (entry.values.length === 0) {
|
|
10174
|
+
continue;
|
|
10175
|
+
}
|
|
10176
|
+
const fragmentSqls = [];
|
|
10177
|
+
let valueIdx = 0;
|
|
10178
|
+
for (const rawValue of entry.values) {
|
|
10179
|
+
const paramName = `p${paramIdx}v${valueIdx}`;
|
|
10180
|
+
const frag = emitOne({
|
|
10181
|
+
param: registered,
|
|
10182
|
+
modifier: entry.modifier,
|
|
10183
|
+
rawValue,
|
|
10184
|
+
paramName,
|
|
10185
|
+
context: opts.context
|
|
10186
|
+
});
|
|
10187
|
+
fragmentSqls.push(frag.sql);
|
|
10188
|
+
for (const p of frag.params) {
|
|
10189
|
+
allParams.push(p);
|
|
10190
|
+
}
|
|
10191
|
+
valueIdx++;
|
|
10192
|
+
}
|
|
10193
|
+
paramIdx++;
|
|
10194
|
+
if (fragmentSqls.length === 1) {
|
|
10195
|
+
groupSqls.push(fragmentSqls[0]);
|
|
10196
|
+
} else {
|
|
10197
|
+
groupSqls.push(`(${fragmentSqls.join(" OR ")})`);
|
|
10198
|
+
}
|
|
10199
|
+
}
|
|
10200
|
+
if (groupSqls.length === 0) {
|
|
10201
|
+
return { sql: "", params: [] };
|
|
10202
|
+
}
|
|
10203
|
+
return { sql: groupSqls.join(" AND "), params: allParams };
|
|
10042
10204
|
}
|
|
10043
10205
|
|
|
10044
|
-
// src/data/operations/
|
|
10045
|
-
var
|
|
10046
|
-
function
|
|
10047
|
-
const datePredicates = buildAppointmentDateSearchPredicateSql(
|
|
10048
|
-
opts.dateConstraints
|
|
10049
|
-
);
|
|
10206
|
+
// src/data/search/operations/generic-search-operation.ts
|
|
10207
|
+
var DEFAULT_LIMIT = 100;
|
|
10208
|
+
function buildGenericSearchSql(opts) {
|
|
10050
10209
|
const lines = [
|
|
10051
10210
|
"SELECT resource_id AS id, resource",
|
|
10052
10211
|
"FROM resources",
|
|
10053
10212
|
"WHERE tenant_id = :tenantId",
|
|
10054
10213
|
" AND workspace_id = :workspaceId",
|
|
10055
|
-
" AND resource_type =
|
|
10214
|
+
" AND resource_type = :resourceType",
|
|
10056
10215
|
" AND deleted_at IS NULL"
|
|
10057
10216
|
];
|
|
10058
|
-
|
|
10059
|
-
lines.push(` AND ${
|
|
10217
|
+
if (opts.combinedSql.length > 0) {
|
|
10218
|
+
lines.push(` AND (${opts.combinedSql})`);
|
|
10060
10219
|
}
|
|
10061
10220
|
lines.push("ORDER BY last_updated DESC");
|
|
10062
10221
|
lines.push("LIMIT :limit;");
|
|
10063
10222
|
return lines.join("\n");
|
|
10064
10223
|
}
|
|
10065
|
-
async function
|
|
10066
|
-
const {
|
|
10067
|
-
if (dateConstraints.length === 0) {
|
|
10068
|
-
throw new Error(
|
|
10069
|
-
"searchAppointmentsByDateOperation requires at least one dateConstraint"
|
|
10070
|
-
);
|
|
10071
|
-
}
|
|
10072
|
-
const { tenantId, workspaceId } = context;
|
|
10224
|
+
async function genericSearchOperation(params) {
|
|
10225
|
+
const { resourceType, tenantId, workspaceId, query, resolver } = params;
|
|
10073
10226
|
const runner = params.runner ?? getDefaultPostgresQueryRunner();
|
|
10074
|
-
const limit = params.limit ??
|
|
10075
|
-
const
|
|
10227
|
+
const limit = params.limit ?? DEFAULT_LIMIT;
|
|
10228
|
+
const registeredParams = resolver(resourceType, tenantId);
|
|
10229
|
+
const context = { tenantId, workspaceId, resourceType };
|
|
10230
|
+
const combined = combineSearchPredicates({
|
|
10231
|
+
query,
|
|
10232
|
+
registeredParams,
|
|
10233
|
+
context
|
|
10234
|
+
});
|
|
10235
|
+
const sql = buildGenericSearchSql({ combinedSql: combined.sql });
|
|
10076
10236
|
const queryParams = [
|
|
10077
10237
|
{ name: "tenantId", value: tenantId },
|
|
10078
10238
|
{ name: "workspaceId", value: workspaceId },
|
|
10239
|
+
{ name: "resourceType", value: resourceType },
|
|
10079
10240
|
{ name: "limit", value: limit },
|
|
10080
|
-
...
|
|
10241
|
+
...combined.params
|
|
10081
10242
|
];
|
|
10082
10243
|
const rows = await runner.query(sql, queryParams);
|
|
10083
10244
|
const entries = rows.map((row) => ({
|
|
10084
10245
|
id: row.id,
|
|
10085
|
-
resource: {
|
|
10086
|
-
...row.resource,
|
|
10087
|
-
id: row.id
|
|
10088
|
-
}
|
|
10246
|
+
resource: { ...row.resource, id: row.id }
|
|
10089
10247
|
}));
|
|
10090
10248
|
return { entries, total: entries.length };
|
|
10091
10249
|
}
|
|
10092
10250
|
|
|
10093
|
-
// src/data/
|
|
10094
|
-
var
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
|
|
10110
|
-
|
|
10111
|
-
}
|
|
10112
|
-
|
|
10113
|
-
|
|
10114
|
-
|
|
10115
|
-
|
|
10116
|
-
|
|
10117
|
-
|
|
10118
|
-
|
|
10119
|
-
|
|
10120
|
-
|
|
10121
|
-
|
|
10122
|
-
|
|
10123
|
-
|
|
10124
|
-
}
|
|
10125
|
-
|
|
10126
|
-
participant: [
|
|
10127
|
-
{
|
|
10128
|
-
actor: {
|
|
10129
|
-
reference: buildOpenHiResourceUrn({
|
|
10130
|
-
tenantId,
|
|
10131
|
-
workspaceId,
|
|
10132
|
-
resourceType: "Patient",
|
|
10133
|
-
resourceId: patientId
|
|
10134
|
-
})
|
|
10135
|
-
}
|
|
10136
|
-
}
|
|
10137
|
-
]
|
|
10138
|
-
});
|
|
10139
|
-
const sql = buildSearchAppointmentsByPatientSql({ dateConstraints });
|
|
10140
|
-
const queryParams = [
|
|
10141
|
-
{ name: "tenantId", value: tenantId },
|
|
10142
|
-
{ name: "workspaceId", value: workspaceId },
|
|
10143
|
-
{ name: "containmentRelative", value: containmentRelative },
|
|
10144
|
-
{ name: "containmentUrn", value: containmentUrn },
|
|
10145
|
-
{ name: "limit", value: limit },
|
|
10146
|
-
...buildAppointmentDateSearchPredicateParams(dateConstraints)
|
|
10147
|
-
];
|
|
10148
|
-
const rows = await runner.query(sql, queryParams);
|
|
10149
|
-
const entries = rows.map((row) => ({
|
|
10150
|
-
id: row.id,
|
|
10151
|
-
resource: {
|
|
10152
|
-
...row.resource,
|
|
10153
|
-
id: row.id
|
|
10154
|
-
}
|
|
10155
|
-
}));
|
|
10156
|
-
return { entries, total: entries.length };
|
|
10157
|
-
}
|
|
10251
|
+
// src/data/search/registry/appointment-search-parameters.ts
|
|
10252
|
+
var APPOINTMENT_SEARCH_PARAMETERS = [
|
|
10253
|
+
{ code: "status", type: "token", jsonbPath: "$.status" },
|
|
10254
|
+
{ code: "date", type: "date", jsonbPath: "$.start" },
|
|
10255
|
+
{
|
|
10256
|
+
code: "patient",
|
|
10257
|
+
type: "reference",
|
|
10258
|
+
jsonbPath: "$.participant[*].actor"
|
|
10259
|
+
},
|
|
10260
|
+
{
|
|
10261
|
+
code: "actor",
|
|
10262
|
+
type: "reference",
|
|
10263
|
+
jsonbPath: "$.participant[*].actor"
|
|
10264
|
+
},
|
|
10265
|
+
{
|
|
10266
|
+
code: "practitioner",
|
|
10267
|
+
type: "reference",
|
|
10268
|
+
jsonbPath: "$.participant[*].actor"
|
|
10269
|
+
},
|
|
10270
|
+
{ code: "service-type", type: "token", jsonbPath: "$.serviceType[*]" },
|
|
10271
|
+
{
|
|
10272
|
+
code: "service-category",
|
|
10273
|
+
type: "token",
|
|
10274
|
+
jsonbPath: "$.serviceCategory[*]"
|
|
10275
|
+
},
|
|
10276
|
+
{ code: "specialty", type: "token", jsonbPath: "$.specialty[*]" },
|
|
10277
|
+
{
|
|
10278
|
+
code: "appointment-type",
|
|
10279
|
+
type: "token",
|
|
10280
|
+
jsonbPath: "$.appointmentType"
|
|
10281
|
+
},
|
|
10282
|
+
{ code: "slot", type: "reference", jsonbPath: "$.slot[*]" }
|
|
10283
|
+
];
|
|
10158
10284
|
|
|
10159
|
-
// src/data/
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
|
|
10285
|
+
// src/data/search/registry/patient-search-parameters.ts
|
|
10286
|
+
var PATIENT_SEARCH_PARAMETERS = [
|
|
10287
|
+
{ code: "gender", type: "token", jsonbPath: "$.gender" },
|
|
10288
|
+
{ code: "identifier", type: "token", jsonbPath: "$.identifier[*].value" },
|
|
10289
|
+
{ code: "birthdate", type: "date", jsonbPath: "$.birthDate" },
|
|
10290
|
+
{
|
|
10291
|
+
code: "name",
|
|
10292
|
+
type: "string",
|
|
10293
|
+
jsonbPath: "$.name[*].text",
|
|
10294
|
+
modifiers: ["exact", "contains", "missing", "not"]
|
|
10295
|
+
},
|
|
10296
|
+
{
|
|
10297
|
+
code: "family",
|
|
10298
|
+
type: "string",
|
|
10299
|
+
jsonbPath: "$.name[*].family",
|
|
10300
|
+
modifiers: ["exact", "contains", "missing", "not"]
|
|
10301
|
+
},
|
|
10302
|
+
{
|
|
10303
|
+
code: "given",
|
|
10304
|
+
type: "string",
|
|
10305
|
+
jsonbPath: "$.name[*].given",
|
|
10306
|
+
modifiers: ["exact", "contains", "missing", "not"]
|
|
10307
|
+
},
|
|
10308
|
+
{ code: "active", type: "token", jsonbPath: "$.active" },
|
|
10309
|
+
{ code: "deceased", type: "token", jsonbPath: "$.deceasedBoolean" },
|
|
10310
|
+
{
|
|
10311
|
+
code: "general-practitioner",
|
|
10312
|
+
type: "reference",
|
|
10313
|
+
jsonbPath: "$.generalPractitioner[*]"
|
|
10314
|
+
},
|
|
10315
|
+
{
|
|
10316
|
+
code: "organization",
|
|
10317
|
+
type: "reference",
|
|
10318
|
+
jsonbPath: "$.managingOrganization"
|
|
10164
10319
|
}
|
|
10165
|
-
|
|
10166
|
-
|
|
10320
|
+
];
|
|
10321
|
+
|
|
10322
|
+
// src/data/search/registry/resolver.ts
|
|
10323
|
+
var STATIC_SEARCH_PARAMETER_MAP = {
|
|
10324
|
+
Appointment: APPOINTMENT_SEARCH_PARAMETERS,
|
|
10325
|
+
Patient: PATIENT_SEARCH_PARAMETERS
|
|
10326
|
+
};
|
|
10327
|
+
var defaultSearchParameterResolver = (resourceType, _tenantId) => STATIC_SEARCH_PARAMETER_MAP[resourceType] ?? [];
|
|
10328
|
+
function getRegisteredSearchParameters(resourceType) {
|
|
10329
|
+
return STATIC_SEARCH_PARAMETER_MAP[resourceType] ?? [];
|
|
10167
10330
|
}
|
|
10168
|
-
|
|
10169
|
-
|
|
10331
|
+
|
|
10332
|
+
// src/data/rest-api/routes/data/appointment/appointment-list-route.ts
|
|
10333
|
+
var APPOINTMENT_RESOURCE_TYPE = "Appointment";
|
|
10334
|
+
function stripModifier(key) {
|
|
10335
|
+
const idx = key.indexOf(":");
|
|
10336
|
+
return idx === -1 ? key : key.slice(0, idx);
|
|
10170
10337
|
}
|
|
10171
|
-
function
|
|
10172
|
-
|
|
10173
|
-
if (raw === void 0) {
|
|
10174
|
-
return [];
|
|
10175
|
-
}
|
|
10176
|
-
const values = Array.isArray(raw) ? raw : [raw];
|
|
10177
|
-
const out = [];
|
|
10178
|
-
for (const v of values) {
|
|
10179
|
-
if (typeof v !== "string") {
|
|
10180
|
-
return { error: "Each ?date= value must be a string." };
|
|
10181
|
-
}
|
|
10182
|
-
const trimmed = v.trim();
|
|
10183
|
-
if (trimmed === "") {
|
|
10184
|
-
return { error: "?date= value must not be empty." };
|
|
10185
|
-
}
|
|
10186
|
-
const prefix = trimmed.slice(0, 2);
|
|
10187
|
-
const datetime = trimmed.slice(2);
|
|
10188
|
-
if (!isAppointmentDateSearchPrefix(prefix)) {
|
|
10189
|
-
return {
|
|
10190
|
-
error: `Unsupported ?date= prefix in "${trimmed}". Supported prefixes: ${APPOINTMENT_DATE_SEARCH_PREFIXES.join(", ")}.`
|
|
10191
|
-
};
|
|
10192
|
-
}
|
|
10193
|
-
if (datetime === "" || Number.isNaN(Date.parse(datetime))) {
|
|
10194
|
-
return { error: `Invalid datetime in ?date=${trimmed}.` };
|
|
10195
|
-
}
|
|
10196
|
-
out.push({ prefix, value: datetime });
|
|
10197
|
-
}
|
|
10198
|
-
return out;
|
|
10338
|
+
function isResultParameter(key) {
|
|
10339
|
+
return key.startsWith("_");
|
|
10199
10340
|
}
|
|
10200
10341
|
function sendInvalidSearch400(res, diagnostics) {
|
|
10201
10342
|
return res.status(400).json({
|
|
@@ -10203,95 +10344,93 @@ function sendInvalidSearch400(res, diagnostics) {
|
|
|
10203
10344
|
issue: [{ severity: "error", code: "invalid", diagnostics }]
|
|
10204
10345
|
});
|
|
10205
10346
|
}
|
|
10347
|
+
function extractSearchParamKeys(query) {
|
|
10348
|
+
const out = [];
|
|
10349
|
+
for (const rawKey of Object.keys(query)) {
|
|
10350
|
+
if (isResultParameter(rawKey)) {
|
|
10351
|
+
continue;
|
|
10352
|
+
}
|
|
10353
|
+
out.push({ rawKey, code: stripModifier(rawKey) });
|
|
10354
|
+
}
|
|
10355
|
+
return out;
|
|
10356
|
+
}
|
|
10357
|
+
function buildUnknownParamDiagnostics(unknownCodes) {
|
|
10358
|
+
const validCodes = getRegisteredSearchParameters(APPOINTMENT_RESOURCE_TYPE).map((p) => p.code).sort().join(", ");
|
|
10359
|
+
const codes = unknownCodes.join(", ");
|
|
10360
|
+
const isPlural = unknownCodes.length !== 1;
|
|
10361
|
+
return [
|
|
10362
|
+
`Unknown search ${isPlural ? "parameters" : "parameter"} for Appointment: ${codes}.`,
|
|
10363
|
+
`Valid codes: ${validCodes}.`
|
|
10364
|
+
].join(" ");
|
|
10365
|
+
}
|
|
10366
|
+
function findMalformedReference(query, searchParamKeys) {
|
|
10367
|
+
const referenceCodes = new Set(
|
|
10368
|
+
getRegisteredSearchParameters(APPOINTMENT_RESOURCE_TYPE).filter((p) => p.type === "reference").map((p) => p.code)
|
|
10369
|
+
);
|
|
10370
|
+
for (const { rawKey, code } of searchParamKeys) {
|
|
10371
|
+
if (!referenceCodes.has(code)) {
|
|
10372
|
+
continue;
|
|
10373
|
+
}
|
|
10374
|
+
const raw = query[rawKey];
|
|
10375
|
+
const values = typeof raw === "string" ? raw.split(",") : Array.isArray(raw) ? raw.flatMap((v) => v.split(",")) : [];
|
|
10376
|
+
for (const v of values) {
|
|
10377
|
+
const trimmed = v.trim();
|
|
10378
|
+
if (trimmed.length === 0) {
|
|
10379
|
+
continue;
|
|
10380
|
+
}
|
|
10381
|
+
if (parseTypedReference(trimmed) === void 0) {
|
|
10382
|
+
return { rawKey, value: trimmed };
|
|
10383
|
+
}
|
|
10384
|
+
}
|
|
10385
|
+
}
|
|
10386
|
+
return void 0;
|
|
10387
|
+
}
|
|
10206
10388
|
async function listAppointmentsRoute(req, res) {
|
|
10207
|
-
const
|
|
10208
|
-
|
|
10209
|
-
|
|
10210
|
-
if (
|
|
10211
|
-
return
|
|
10389
|
+
const searchParamKeys = extractSearchParamKeys(
|
|
10390
|
+
req.query
|
|
10391
|
+
);
|
|
10392
|
+
if (searchParamKeys.length === 0) {
|
|
10393
|
+
return handleListRoute({
|
|
10394
|
+
req,
|
|
10395
|
+
res,
|
|
10396
|
+
basePath: BASE_PATH.APPOINTMENT,
|
|
10397
|
+
listOperation: listAppointmentsOperation,
|
|
10398
|
+
errorLogContext: "GET /Appointment list error:"
|
|
10399
|
+
});
|
|
10212
10400
|
}
|
|
10213
|
-
const
|
|
10214
|
-
|
|
10401
|
+
const registered = getRegisteredSearchParameters(APPOINTMENT_RESOURCE_TYPE);
|
|
10402
|
+
const validCodes = new Set(registered.map((p) => p.code));
|
|
10403
|
+
const unknownCodes = searchParamKeys.map((k) => k.code).filter((code) => !validCodes.has(code));
|
|
10404
|
+
if (unknownCodes.length > 0) {
|
|
10215
10405
|
return sendInvalidSearch400(
|
|
10216
10406
|
res,
|
|
10217
|
-
|
|
10407
|
+
buildUnknownParamDiagnostics([...new Set(unknownCodes)])
|
|
10218
10408
|
);
|
|
10219
10409
|
}
|
|
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
|
-
}
|
|
10410
|
+
const malformedRef = findMalformedReference(
|
|
10411
|
+
req.query,
|
|
10412
|
+
searchParamKeys
|
|
10413
|
+
);
|
|
10414
|
+
if (malformedRef !== void 0) {
|
|
10415
|
+
return sendInvalidSearch400(
|
|
10416
|
+
res,
|
|
10417
|
+
`?${malformedRef.rawKey} must be a typed reference like "Practitioner/<id>"; got "${malformedRef.value}".`
|
|
10418
|
+
);
|
|
10267
10419
|
}
|
|
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
|
-
}
|
|
10420
|
+
const ctx = req.openhiContext;
|
|
10421
|
+
try {
|
|
10422
|
+
const result = await genericSearchOperation({
|
|
10423
|
+
resourceType: APPOINTMENT_RESOURCE_TYPE,
|
|
10424
|
+
tenantId: ctx.tenantId,
|
|
10425
|
+
workspaceId: ctx.workspaceId,
|
|
10426
|
+
query: req.query,
|
|
10427
|
+
resolver: defaultSearchParameterResolver
|
|
10428
|
+
});
|
|
10429
|
+
const bundle = buildSearchsetBundle(BASE_PATH.APPOINTMENT, result.entries);
|
|
10430
|
+
return res.json(bundle);
|
|
10431
|
+
} catch (err) {
|
|
10432
|
+
return sendOperationOutcome500(res, err, "GET /Appointment search error:");
|
|
10287
10433
|
}
|
|
10288
|
-
return handleListRoute({
|
|
10289
|
-
req,
|
|
10290
|
-
res,
|
|
10291
|
-
basePath: BASE_PATH.APPOINTMENT,
|
|
10292
|
-
listOperation: listAppointmentsOperation,
|
|
10293
|
-
errorLogContext: "GET /Appointment list error:"
|
|
10294
|
-
});
|
|
10295
10434
|
}
|
|
10296
10435
|
|
|
10297
10436
|
// src/data/operations/data/appointment/appointment-update-operation.ts
|
|
@@ -17886,7 +18025,7 @@ function buildPeriodSearchPredicateParams(constraints) {
|
|
|
17886
18025
|
}
|
|
17887
18026
|
|
|
17888
18027
|
// src/data/operations/data/encounter/encounter-search-by-date-operation.ts
|
|
17889
|
-
var
|
|
18028
|
+
var DEFAULT_LIMIT2 = 100;
|
|
17890
18029
|
function buildSearchEncountersByDateSql(opts) {
|
|
17891
18030
|
const periodPredicates = buildPeriodSearchPredicateSql(
|
|
17892
18031
|
opts.periodConstraints
|
|
@@ -17915,7 +18054,7 @@ async function searchEncountersByDateOperation(params) {
|
|
|
17915
18054
|
}
|
|
17916
18055
|
const { tenantId, workspaceId } = context;
|
|
17917
18056
|
const runner = params.runner ?? getDefaultPostgresQueryRunner();
|
|
17918
|
-
const limit = params.limit ??
|
|
18057
|
+
const limit = params.limit ?? DEFAULT_LIMIT2;
|
|
17919
18058
|
const sql = buildSearchEncountersByDateSql({ periodConstraints });
|
|
17920
18059
|
const queryParams = [
|
|
17921
18060
|
{ name: "tenantId", value: tenantId },
|
|
@@ -17935,7 +18074,7 @@ async function searchEncountersByDateOperation(params) {
|
|
|
17935
18074
|
}
|
|
17936
18075
|
|
|
17937
18076
|
// src/data/operations/data/encounter/encounter-search-by-participant-operation.ts
|
|
17938
|
-
var
|
|
18077
|
+
var DEFAULT_LIMIT3 = 100;
|
|
17939
18078
|
function buildSearchEncountersByParticipantSql(opts) {
|
|
17940
18079
|
const periodPredicates = buildPeriodSearchPredicateSql(
|
|
17941
18080
|
opts?.periodConstraints ?? []
|
|
@@ -17961,7 +18100,7 @@ async function searchEncountersByParticipantOperation(params) {
|
|
|
17961
18100
|
const periodConstraints = params.periodConstraints ?? [];
|
|
17962
18101
|
const { tenantId, workspaceId } = context;
|
|
17963
18102
|
const runner = params.runner ?? getDefaultPostgresQueryRunner();
|
|
17964
|
-
const limit = params.limit ??
|
|
18103
|
+
const limit = params.limit ?? DEFAULT_LIMIT3;
|
|
17965
18104
|
const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
|
|
17966
18105
|
shape: {
|
|
17967
18106
|
kind: "array-of-objects",
|
|
@@ -17993,7 +18132,7 @@ async function searchEncountersByParticipantOperation(params) {
|
|
|
17993
18132
|
}
|
|
17994
18133
|
|
|
17995
18134
|
// src/data/operations/data/encounter/encounter-search-by-patient-operation.ts
|
|
17996
|
-
var
|
|
18135
|
+
var DEFAULT_LIMIT4 = 100;
|
|
17997
18136
|
function buildSearchEncountersByPatientSql(opts) {
|
|
17998
18137
|
const periodPredicates = buildPeriodSearchPredicateSql(
|
|
17999
18138
|
opts?.periodConstraints ?? []
|
|
@@ -18020,7 +18159,7 @@ async function searchEncountersByPatientOperation(params) {
|
|
|
18020
18159
|
const periodConstraints = params.periodConstraints ?? [];
|
|
18021
18160
|
const { tenantId, workspaceId } = context;
|
|
18022
18161
|
const runner = params.runner ?? getDefaultPostgresQueryRunner();
|
|
18023
|
-
const limit = params.limit ??
|
|
18162
|
+
const limit = params.limit ?? DEFAULT_LIMIT4;
|
|
18024
18163
|
const containmentRelative = JSON.stringify({
|
|
18025
18164
|
subject: { reference: `Patient/${patientId}` }
|
|
18026
18165
|
});
|
|
@@ -18055,7 +18194,7 @@ async function searchEncountersByPatientOperation(params) {
|
|
|
18055
18194
|
}
|
|
18056
18195
|
|
|
18057
18196
|
// src/data/rest-api/routes/data/encounter/encounter-list-route.ts
|
|
18058
|
-
function
|
|
18197
|
+
function singleStringQueryParam(req, name) {
|
|
18059
18198
|
const v = req.query[name];
|
|
18060
18199
|
if (typeof v !== "string") {
|
|
18061
18200
|
return void 0;
|
|
@@ -18063,7 +18202,7 @@ function singleStringQueryParam2(req, name) {
|
|
|
18063
18202
|
const trimmed = v.trim();
|
|
18064
18203
|
return trimmed === "" ? void 0 : trimmed;
|
|
18065
18204
|
}
|
|
18066
|
-
function
|
|
18205
|
+
function isError(v) {
|
|
18067
18206
|
return v.error !== void 0;
|
|
18068
18207
|
}
|
|
18069
18208
|
function parseEncounterDateConstraints(req) {
|
|
@@ -18102,10 +18241,10 @@ function sendInvalidSearch4002(res, diagnostics) {
|
|
|
18102
18241
|
});
|
|
18103
18242
|
}
|
|
18104
18243
|
async function listEncountersRoute(req, res) {
|
|
18105
|
-
const patientId =
|
|
18106
|
-
const participantRef =
|
|
18244
|
+
const patientId = singleStringQueryParam(req, "patient");
|
|
18245
|
+
const participantRef = singleStringQueryParam(req, "participant");
|
|
18107
18246
|
const parsed = parseEncounterDateConstraints(req);
|
|
18108
|
-
if (
|
|
18247
|
+
if (isError(parsed)) {
|
|
18109
18248
|
return sendInvalidSearch4002(res, parsed.error);
|
|
18110
18249
|
}
|
|
18111
18250
|
const periodConstraints = parsed;
|
|
@@ -29821,487 +29960,104 @@ async function createPatientOperation(params) {
|
|
|
29821
29960
|
date
|
|
29822
29961
|
);
|
|
29823
29962
|
}
|
|
29824
|
-
|
|
29825
|
-
// src/data/rest-api/routes/data/patient/patient-create-route.ts
|
|
29826
|
-
async function createPatientRoute(req, res) {
|
|
29827
|
-
const bodyResult = requireJsonBodyAs(req, res);
|
|
29828
|
-
if ("errorResponse" in bodyResult) return bodyResult.errorResponse;
|
|
29829
|
-
const ctx = req.openhiContext;
|
|
29830
|
-
const body = bodyResult.body;
|
|
29831
|
-
const patient = {
|
|
29832
|
-
...body,
|
|
29833
|
-
resourceType: "Patient"
|
|
29834
|
-
};
|
|
29835
|
-
try {
|
|
29836
|
-
const result = await createPatientOperation({
|
|
29837
|
-
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
|
-
});
|
|
29963
|
+
|
|
29964
|
+
// src/data/rest-api/routes/data/patient/patient-create-route.ts
|
|
29965
|
+
async function createPatientRoute(req, res) {
|
|
29966
|
+
const bodyResult = requireJsonBodyAs(req, res);
|
|
29967
|
+
if ("errorResponse" in bodyResult) return bodyResult.errorResponse;
|
|
29968
|
+
const ctx = req.openhiContext;
|
|
29969
|
+
const body = bodyResult.body;
|
|
29970
|
+
const patient = {
|
|
29971
|
+
...body,
|
|
29972
|
+
resourceType: "Patient"
|
|
29973
|
+
};
|
|
29974
|
+
try {
|
|
29975
|
+
const result = await createPatientOperation({
|
|
29976
|
+
context: ctx,
|
|
29977
|
+
body: patient
|
|
29978
|
+
});
|
|
29979
|
+
return res.status(201).location(`${BASE_PATH.PATIENT}/${result.id}`).json(result.resource);
|
|
29980
|
+
} catch (err) {
|
|
29981
|
+
return sendOperationOutcome500(res, err, "POST Patient error:");
|
|
30159
29982
|
}
|
|
30160
29983
|
}
|
|
30161
|
-
|
|
30162
|
-
|
|
30163
|
-
|
|
30164
|
-
const
|
|
30165
|
-
|
|
30166
|
-
|
|
30167
|
-
|
|
30168
|
-
|
|
30169
|
-
|
|
30170
|
-
|
|
30171
|
-
|
|
30172
|
-
|
|
30173
|
-
}
|
|
30174
|
-
if (entry.values.length === 0) {
|
|
30175
|
-
continue;
|
|
30176
|
-
}
|
|
30177
|
-
const fragmentSqls = [];
|
|
30178
|
-
let valueIdx = 0;
|
|
30179
|
-
for (const rawValue of entry.values) {
|
|
30180
|
-
const paramName = `p${paramIdx}v${valueIdx}`;
|
|
30181
|
-
const frag = emitOne({
|
|
30182
|
-
param: registered,
|
|
30183
|
-
modifier: entry.modifier,
|
|
30184
|
-
rawValue,
|
|
30185
|
-
paramName,
|
|
30186
|
-
context: opts.context
|
|
30187
|
-
});
|
|
30188
|
-
fragmentSqls.push(frag.sql);
|
|
30189
|
-
for (const p of frag.params) {
|
|
30190
|
-
allParams.push(p);
|
|
30191
|
-
}
|
|
30192
|
-
valueIdx++;
|
|
30193
|
-
}
|
|
30194
|
-
paramIdx++;
|
|
30195
|
-
if (fragmentSqls.length === 1) {
|
|
30196
|
-
groupSqls.push(fragmentSqls[0]);
|
|
30197
|
-
} else {
|
|
30198
|
-
groupSqls.push(`(${fragmentSqls.join(" OR ")})`);
|
|
30199
|
-
}
|
|
30200
|
-
}
|
|
30201
|
-
if (groupSqls.length === 0) {
|
|
30202
|
-
return { sql: "", params: [] };
|
|
30203
|
-
}
|
|
30204
|
-
return { sql: groupSqls.join(" AND "), params: allParams };
|
|
29984
|
+
|
|
29985
|
+
// src/data/operations/data/patient/patient-delete-operation.ts
|
|
29986
|
+
async function deletePatientOperation(params) {
|
|
29987
|
+
const { context, id, tableName } = params;
|
|
29988
|
+
const { tenantId, workspaceId } = context;
|
|
29989
|
+
const service = getDynamoDataService(tableName);
|
|
29990
|
+
await deleteDataEntityById(
|
|
29991
|
+
service.entities.patient,
|
|
29992
|
+
tenantId,
|
|
29993
|
+
workspaceId,
|
|
29994
|
+
id
|
|
29995
|
+
);
|
|
30205
29996
|
}
|
|
30206
29997
|
|
|
30207
|
-
// src/data/
|
|
30208
|
-
|
|
30209
|
-
|
|
30210
|
-
const
|
|
30211
|
-
|
|
30212
|
-
|
|
30213
|
-
|
|
30214
|
-
|
|
30215
|
-
"
|
|
30216
|
-
" AND deleted_at IS NULL"
|
|
30217
|
-
];
|
|
30218
|
-
if (opts.combinedSql.length > 0) {
|
|
30219
|
-
lines.push(` AND (${opts.combinedSql})`);
|
|
29998
|
+
// src/data/rest-api/routes/data/patient/patient-delete-route.ts
|
|
29999
|
+
async function deletePatientRoute(req, res) {
|
|
30000
|
+
const id = String(req.params.id);
|
|
30001
|
+
const ctx = req.openhiContext;
|
|
30002
|
+
try {
|
|
30003
|
+
await deletePatientOperation({ context: ctx, id });
|
|
30004
|
+
return res.status(204).send();
|
|
30005
|
+
} catch (err) {
|
|
30006
|
+
return sendOperationOutcome500(res, err, "DELETE Patient error:");
|
|
30220
30007
|
}
|
|
30221
|
-
lines.push("ORDER BY last_updated DESC");
|
|
30222
|
-
lines.push("LIMIT :limit;");
|
|
30223
|
-
return lines.join("\n");
|
|
30224
30008
|
}
|
|
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 };
|
|
30009
|
+
|
|
30010
|
+
// src/data/operations/data/patient/patient-get-by-id-operation.ts
|
|
30011
|
+
async function getPatientByIdOperation(params) {
|
|
30012
|
+
const { context, id, tableName } = params;
|
|
30013
|
+
const { tenantId, workspaceId } = context;
|
|
30014
|
+
const service = getDynamoDataService(tableName);
|
|
30015
|
+
return getDataEntityById(
|
|
30016
|
+
service.entities.patient,
|
|
30017
|
+
tenantId,
|
|
30018
|
+
workspaceId,
|
|
30019
|
+
id,
|
|
30020
|
+
"Patient"
|
|
30021
|
+
);
|
|
30250
30022
|
}
|
|
30251
30023
|
|
|
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"
|
|
30024
|
+
// src/data/rest-api/routes/data/patient/patient-get-by-id-route.ts
|
|
30025
|
+
async function getPatientByIdRoute(req, res) {
|
|
30026
|
+
const id = String(req.params.id);
|
|
30027
|
+
const ctx = req.openhiContext;
|
|
30028
|
+
try {
|
|
30029
|
+
const result = await getPatientByIdOperation({ context: ctx, id });
|
|
30030
|
+
return res.json(result.resource);
|
|
30031
|
+
} catch (err) {
|
|
30032
|
+
const status = domainErrorToHttpStatus(err);
|
|
30033
|
+
if (status === 404) {
|
|
30034
|
+
const diagnostics = err instanceof NotFoundError ? err.message : `Patient ${id} not found`;
|
|
30035
|
+
return sendOperationOutcome404(res, diagnostics);
|
|
30036
|
+
}
|
|
30037
|
+
return sendOperationOutcome500(res, err, "GET Patient error:");
|
|
30286
30038
|
}
|
|
30287
|
-
|
|
30039
|
+
}
|
|
30288
30040
|
|
|
30289
|
-
// src/data/
|
|
30290
|
-
|
|
30291
|
-
|
|
30292
|
-
};
|
|
30293
|
-
|
|
30294
|
-
|
|
30295
|
-
|
|
30041
|
+
// src/data/operations/data/patient/patient-list-operation.ts
|
|
30042
|
+
async function listPatientsOperation(params) {
|
|
30043
|
+
const { context, tableName, mode } = params;
|
|
30044
|
+
const { tenantId, workspaceId } = context;
|
|
30045
|
+
const service = getDynamoDataService(tableName);
|
|
30046
|
+
return listDataEntitiesByWorkspace(
|
|
30047
|
+
service.entities.patient,
|
|
30048
|
+
tenantId,
|
|
30049
|
+
workspaceId,
|
|
30050
|
+
mode
|
|
30051
|
+
);
|
|
30296
30052
|
}
|
|
30297
30053
|
|
|
30298
30054
|
// src/data/rest-api/routes/data/patient/patient-list-route.ts
|
|
30299
30055
|
var PATIENT_RESOURCE_TYPE = "Patient";
|
|
30300
|
-
function
|
|
30056
|
+
function stripModifier2(key) {
|
|
30301
30057
|
const idx = key.indexOf(":");
|
|
30302
30058
|
return idx === -1 ? key : key.slice(0, idx);
|
|
30303
30059
|
}
|
|
30304
|
-
function
|
|
30060
|
+
function isResultParameter2(key) {
|
|
30305
30061
|
return key.startsWith("_");
|
|
30306
30062
|
}
|
|
30307
30063
|
function sendInvalidSearch4003(res, diagnostics) {
|
|
@@ -30310,17 +30066,17 @@ function sendInvalidSearch4003(res, diagnostics) {
|
|
|
30310
30066
|
issue: [{ severity: "error", code: "invalid", diagnostics }]
|
|
30311
30067
|
});
|
|
30312
30068
|
}
|
|
30313
|
-
function
|
|
30069
|
+
function extractSearchParamKeys2(query) {
|
|
30314
30070
|
const out = [];
|
|
30315
30071
|
for (const rawKey of Object.keys(query)) {
|
|
30316
|
-
if (
|
|
30072
|
+
if (isResultParameter2(rawKey)) {
|
|
30317
30073
|
continue;
|
|
30318
30074
|
}
|
|
30319
|
-
out.push({ rawKey, code:
|
|
30075
|
+
out.push({ rawKey, code: stripModifier2(rawKey) });
|
|
30320
30076
|
}
|
|
30321
30077
|
return out;
|
|
30322
30078
|
}
|
|
30323
|
-
function
|
|
30079
|
+
function buildUnknownParamDiagnostics2(unknownCodes) {
|
|
30324
30080
|
const validCodes = getRegisteredSearchParameters(PATIENT_RESOURCE_TYPE).map((p) => p.code).sort().join(", ");
|
|
30325
30081
|
const codes = unknownCodes.join(", ");
|
|
30326
30082
|
const isPlural = unknownCodes.length !== 1;
|
|
@@ -30329,7 +30085,7 @@ function buildUnknownParamDiagnostics(unknownCodes) {
|
|
|
30329
30085
|
`Valid codes: ${validCodes}.`
|
|
30330
30086
|
].join(" ");
|
|
30331
30087
|
}
|
|
30332
|
-
function
|
|
30088
|
+
function findMalformedReference2(query, searchParamKeys) {
|
|
30333
30089
|
const referenceCodes = new Set(
|
|
30334
30090
|
getRegisteredSearchParameters(PATIENT_RESOURCE_TYPE).filter((p) => p.type === "reference").map((p) => p.code)
|
|
30335
30091
|
);
|
|
@@ -30352,7 +30108,7 @@ function findMalformedReference(query, searchParamKeys) {
|
|
|
30352
30108
|
return void 0;
|
|
30353
30109
|
}
|
|
30354
30110
|
async function listPatientsRoute(req, res) {
|
|
30355
|
-
const searchParamKeys =
|
|
30111
|
+
const searchParamKeys = extractSearchParamKeys2(
|
|
30356
30112
|
req.query
|
|
30357
30113
|
);
|
|
30358
30114
|
if (searchParamKeys.length === 0) {
|
|
@@ -30370,10 +30126,10 @@ async function listPatientsRoute(req, res) {
|
|
|
30370
30126
|
if (unknownCodes.length > 0) {
|
|
30371
30127
|
return sendInvalidSearch4003(
|
|
30372
30128
|
res,
|
|
30373
|
-
|
|
30129
|
+
buildUnknownParamDiagnostics2([...new Set(unknownCodes)])
|
|
30374
30130
|
);
|
|
30375
30131
|
}
|
|
30376
|
-
const malformedRef =
|
|
30132
|
+
const malformedRef = findMalformedReference2(
|
|
30377
30133
|
req.query,
|
|
30378
30134
|
searchParamKeys
|
|
30379
30135
|
);
|
|
@@ -34216,7 +33972,7 @@ async function listSchedulesOperation(params) {
|
|
|
34216
33972
|
}
|
|
34217
33973
|
|
|
34218
33974
|
// src/data/operations/data/schedule/schedule-search-by-actor-operation.ts
|
|
34219
|
-
var
|
|
33975
|
+
var DEFAULT_LIMIT5 = 100;
|
|
34220
33976
|
function buildSearchSchedulesByActorSql() {
|
|
34221
33977
|
return [
|
|
34222
33978
|
"SELECT resource_id AS id, resource",
|
|
@@ -34234,7 +33990,7 @@ async function searchSchedulesByActorOperation(params) {
|
|
|
34234
33990
|
const { context, actorReference } = params;
|
|
34235
33991
|
const { tenantId, workspaceId } = context;
|
|
34236
33992
|
const runner = params.runner ?? getDefaultPostgresQueryRunner();
|
|
34237
|
-
const limit = params.limit ??
|
|
33993
|
+
const limit = params.limit ?? DEFAULT_LIMIT5;
|
|
34238
33994
|
const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
|
|
34239
33995
|
shape: { kind: "array-of-references", field: "actor" },
|
|
34240
33996
|
reference: actorReference,
|
|
@@ -34260,7 +34016,7 @@ async function searchSchedulesByActorOperation(params) {
|
|
|
34260
34016
|
}
|
|
34261
34017
|
|
|
34262
34018
|
// src/data/rest-api/routes/data/schedule/schedule-list-route.ts
|
|
34263
|
-
function
|
|
34019
|
+
function singleStringQueryParam2(req, name) {
|
|
34264
34020
|
const v = req.query[name];
|
|
34265
34021
|
if (typeof v !== "string") {
|
|
34266
34022
|
return void 0;
|
|
@@ -34275,7 +34031,7 @@ function sendInvalidSearch4004(res, diagnostics) {
|
|
|
34275
34031
|
});
|
|
34276
34032
|
}
|
|
34277
34033
|
async function listSchedulesRoute(req, res) {
|
|
34278
|
-
const actorRef =
|
|
34034
|
+
const actorRef = singleStringQueryParam2(req, "actor");
|
|
34279
34035
|
if (actorRef !== void 0) {
|
|
34280
34036
|
if (parseTypedReference(actorRef) === void 0) {
|
|
34281
34037
|
return sendInvalidSearch4004(
|
|
@@ -37980,7 +37736,7 @@ async function listTasksOperation(params) {
|
|
|
37980
37736
|
}
|
|
37981
37737
|
|
|
37982
37738
|
// src/data/operations/data/task/task-search-by-owner-operation.ts
|
|
37983
|
-
var
|
|
37739
|
+
var DEFAULT_LIMIT6 = 100;
|
|
37984
37740
|
function buildSearchTasksByOwnerSql() {
|
|
37985
37741
|
return [
|
|
37986
37742
|
"SELECT resource_id AS id, resource",
|
|
@@ -37998,7 +37754,7 @@ async function searchTasksByOwnerOperation(params) {
|
|
|
37998
37754
|
const { context, ownerReference } = params;
|
|
37999
37755
|
const { tenantId, workspaceId } = context;
|
|
38000
37756
|
const runner = params.runner ?? getDefaultPostgresQueryRunner();
|
|
38001
|
-
const limit = params.limit ??
|
|
37757
|
+
const limit = params.limit ?? DEFAULT_LIMIT6;
|
|
38002
37758
|
const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
|
|
38003
37759
|
shape: { kind: "scalar", field: "owner" },
|
|
38004
37760
|
reference: ownerReference,
|
|
@@ -38024,7 +37780,7 @@ async function searchTasksByOwnerOperation(params) {
|
|
|
38024
37780
|
}
|
|
38025
37781
|
|
|
38026
37782
|
// src/data/operations/data/task/task-search-by-requester-operation.ts
|
|
38027
|
-
var
|
|
37783
|
+
var DEFAULT_LIMIT7 = 100;
|
|
38028
37784
|
function buildSearchTasksByRequesterSql() {
|
|
38029
37785
|
return [
|
|
38030
37786
|
"SELECT resource_id AS id, resource",
|
|
@@ -38042,7 +37798,7 @@ async function searchTasksByRequesterOperation(params) {
|
|
|
38042
37798
|
const { context, requesterReference } = params;
|
|
38043
37799
|
const { tenantId, workspaceId } = context;
|
|
38044
37800
|
const runner = params.runner ?? getDefaultPostgresQueryRunner();
|
|
38045
|
-
const limit = params.limit ??
|
|
37801
|
+
const limit = params.limit ?? DEFAULT_LIMIT7;
|
|
38046
37802
|
const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
|
|
38047
37803
|
shape: { kind: "scalar", field: "requester" },
|
|
38048
37804
|
reference: requesterReference,
|
|
@@ -38068,7 +37824,7 @@ async function searchTasksByRequesterOperation(params) {
|
|
|
38068
37824
|
}
|
|
38069
37825
|
|
|
38070
37826
|
// src/data/rest-api/routes/data/task/task-list-route.ts
|
|
38071
|
-
function
|
|
37827
|
+
function singleStringQueryParam3(req, name) {
|
|
38072
37828
|
const v = req.query[name];
|
|
38073
37829
|
if (typeof v !== "string") {
|
|
38074
37830
|
return void 0;
|
|
@@ -38083,8 +37839,8 @@ function sendInvalidSearch4005(res, diagnostics) {
|
|
|
38083
37839
|
});
|
|
38084
37840
|
}
|
|
38085
37841
|
async function listTasksRoute(req, res) {
|
|
38086
|
-
const ownerRef =
|
|
38087
|
-
const requesterRef =
|
|
37842
|
+
const ownerRef = singleStringQueryParam3(req, "owner");
|
|
37843
|
+
const requesterRef = singleStringQueryParam3(req, "requester");
|
|
38088
37844
|
if (ownerRef !== void 0 && requesterRef !== void 0) {
|
|
38089
37845
|
return sendInvalidSearch4005(
|
|
38090
37846
|
res,
|