@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.
@@ -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 match = /^\$\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonbPath);
9789
- if (!match) {
9790
- throw new Error(
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
- return `resource->>'${match[1]}'`;
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
- var DEFAULT_INTERVAL_PARAM_PREFIX = "intervalDateConstraint";
9831
- function intervalConstraintParamName(prefix, index) {
9832
- return `${prefix}${index}`;
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 buildIntervalSingleSql(prefix, startExtract, endExtract, paramName) {
9835
- switch (prefix) {
9836
- case "eq":
9837
- return `(${startExtract} IS NOT NULL AND ${startExtract} = :${paramName})`;
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})`;
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
- fragments.push(`(${startExtract} IS NOT NULL OR ${endExtract} IS NOT NULL)`);
9868
- return fragments;
9869
- }
9870
- function buildIntervalDateSearchPredicateParams(constraints, opts) {
9871
- const paramPrefix = opts?.paramNamePrefix ?? DEFAULT_INTERVAL_PARAM_PREFIX;
9872
- return constraints.map((c, i) => ({
9873
- name: intervalConstraintParamName(paramPrefix, i),
9874
- value: c.value
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 buildAppointmentDateSearchPredicateSql(constraints) {
9891
- return buildIntervalDateSearchPredicateSql(constraints, {
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
- var REFERENCE_CONTAINMENT_SQL_FRAGMENT = "(resource @> :containmentRelative::jsonb OR resource @> :containmentUrn::jsonb)";
9908
- function parseTypedReference(s) {
9909
- const match = /^([A-Za-z][A-Za-z0-9_]*)\/([^\s/]+)$/.exec(s);
9910
- if (!match) {
9911
- return void 0;
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 wrapReferenceInShape(shape, reference) {
9960
+ function buildIlikeExtractSql(shape, paramName) {
9916
9961
  switch (shape.kind) {
9917
9962
  case "scalar":
9918
- return { [shape.field]: { reference } };
9919
- case "array-of-references":
9920
- return { [shape.field]: [{ reference }] };
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 { [shape.field]: [{ [shape.subfield]: { reference } }] };
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 buildReferenceContainmentPayload(params) {
9926
- const parsed = parseTypedReference(params.reference);
9927
- if (!parsed) {
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
- `Reference "${params.reference}" is not a valid typed reference (<ResourceType>/<id>).`
9991
+ `String predicate does not support modifier ":${modifier}". Supported: ${STRING_MODIFIERS.map((m) => `:${m}`).join(", ")}.`
9930
9992
  );
9931
9993
  }
9932
- const urn = buildOpenHiResourceUrn({
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
- function jsonbPathToReferenceShape(jsonbPath) {
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 arrayOfRefs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(jsonbPath);
9951
- if (arrayOfRefs) {
9952
- return { kind: "array-of-references", field: arrayOfRefs[1] };
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
- `Reference predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
10018
+ `Token predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
9966
10019
  );
9967
10020
  }
9968
- function emitReferencePredicate(opts) {
9969
- const shape = jsonbPathToReferenceShape(opts.jsonbPath);
9970
- const { containmentRelative, containmentUrn } = buildReferenceContainmentPayload({
9971
- shape,
9972
- reference: opts.rawValue,
9973
- tenantId: opts.context.tenantId,
9974
- workspaceId: opts.context.workspaceId
9975
- });
9976
- const relName = `${opts.paramName}R`;
9977
- const urnName = `${opts.paramName}U`;
9978
- const sql = `(resource @> :${relName}::jsonb OR resource @> :${urnName}::jsonb)`;
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: relName, value: containmentRelative },
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/operations/data/appointment/appointment-search-by-actor-operation.ts
9987
- var DEFAULT_LIMIT = 100;
9988
- function buildSearchAppointmentsByActorSql(opts) {
9989
- const datePredicates = buildAppointmentDateSearchPredicateSql(
9990
- opts?.dateConstraints ?? []
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
- lines.push("ORDER BY last_updated DESC");
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
- async function searchAppointmentsByActorOperation(params) {
10009
- const { context, actorReference } = params;
10010
- const dateConstraints = params.dateConstraints ?? [];
10011
- const { tenantId, workspaceId } = context;
10012
- const runner = params.runner ?? getDefaultPostgresQueryRunner();
10013
- const limit = params.limit ?? DEFAULT_LIMIT;
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 { entries, total: entries.length };
10066
+ }
10067
+ return out;
10042
10068
  }
10043
-
10044
- // src/data/operations/data/appointment/appointment-search-by-date-operation.ts
10045
- var DEFAULT_LIMIT2 = 100;
10046
- function buildSearchAppointmentsByDateSql(opts) {
10047
- const datePredicates = buildAppointmentDateSearchPredicateSql(
10048
- opts.dateConstraints
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
- const lines = [
10051
- "SELECT resource_id AS id, resource",
10052
- "FROM resources",
10053
- "WHERE tenant_id = :tenantId",
10054
- " AND workspace_id = :workspaceId",
10055
- " AND resource_type = 'Appointment'",
10056
- " AND deleted_at IS NULL"
10057
- ];
10058
- for (const fragment of datePredicates) {
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
- lines.push("ORDER BY last_updated DESC");
10062
- lines.push("LIMIT :limit;");
10063
- return lines.join("\n");
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
- async function searchAppointmentsByDateOperation(params) {
10066
- const { context, dateConstraints } = params;
10067
- if (dateConstraints.length === 0) {
10068
- throw new Error(
10069
- "searchAppointmentsByDateOperation requires at least one dateConstraint"
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
- const { tenantId, workspaceId } = context;
10073
- const runner = params.runner ?? getDefaultPostgresQueryRunner();
10074
- const limit = params.limit ?? DEFAULT_LIMIT2;
10075
- const sql = buildSearchAppointmentsByDateSql({ dateConstraints });
10076
- const queryParams = [
10077
- { name: "tenantId", value: tenantId },
10078
- { name: "workspaceId", value: workspaceId },
10079
- { name: "limit", value: limit },
10080
- ...buildAppointmentDateSearchPredicateParams(dateConstraints)
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
- return { entries, total: entries.length };
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/data/appointment/appointment-search-by-patient-operation.ts
10094
- var DEFAULT_LIMIT3 = 100;
10095
- function buildSearchAppointmentsByPatientSql(opts) {
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 = 'Appointment'",
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
- for (const fragment of datePredicates) {
10110
- lines.push(` AND ${fragment}`);
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 searchAppointmentsByPatientOperation(params) {
10117
- const { context, patientId } = params;
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 ?? DEFAULT_LIMIT3;
10122
- const containmentRelative = JSON.stringify({
10123
- participant: [{ actor: { reference: `Patient/${patientId}` } }]
10124
- });
10125
- const containmentUrn = JSON.stringify({
10126
- participant: [
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 = buildSearchAppointmentsByPatientSql({ dateConstraints });
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: "containmentRelative", value: containmentRelative },
10144
- { name: "containmentUrn", value: containmentUrn },
10243
+ { name: "resourceType", value: resourceType },
10145
10244
  { name: "limit", value: limit },
10146
- ...buildAppointmentDateSearchPredicateParams(dateConstraints)
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/rest-api/routes/data/appointment/appointment-list-route.ts
10160
- function singleStringQueryParam(req, name) {
10161
- const v = req.query[name];
10162
- if (typeof v !== "string") {
10163
- return void 0;
10164
- }
10165
- const trimmed = v.trim();
10166
- return trimmed === "" ? void 0 : trimmed;
10167
- }
10168
- function isError(v) {
10169
- return v.error !== void 0;
10170
- }
10171
- function parseAppointmentDateConstraints(req) {
10172
- const raw = req.query.date;
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 });
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
- return out;
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 patientId = singleStringQueryParam(req, "patient");
10208
- const actorRef = singleStringQueryParam(req, "actor");
10209
- const parsed = parseAppointmentDateConstraints(req);
10210
- if (isError(parsed)) {
10211
- return sendInvalidSearch400(res, parsed.error);
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 dateConstraints = parsed;
10214
- if (patientId !== void 0 && actorRef !== void 0) {
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
- "?patient= and ?actor= cannot be combined on the same request."
10487
+ buildUnknownParamDiagnostics([...new Set(unknownCodes)])
10218
10488
  );
10219
10489
  }
10220
- if (actorRef !== void 0) {
10221
- if (parseTypedReference(actorRef) === void 0) {
10222
- return sendInvalidSearch400(
10223
- res,
10224
- `?actor must be a typed reference like "Practitioner/<id>"; got "${actorRef}".`
10225
- );
10226
- }
10227
- const ctx = req.openhiContext;
10228
- try {
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
- if (dateConstraints.length > 0) {
10269
- const ctx = req.openhiContext;
10270
- try {
10271
- const result = await searchAppointmentsByDateOperation({
10272
- context: ctx,
10273
- dateConstraints
10274
- });
10275
- const bundle = buildSearchsetBundle(
10276
- BASE_PATH.APPOINTMENT,
10277
- result.entries
10278
- );
10279
- return res.json(bundle);
10280
- } catch (err) {
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/operations/data/encounter/encounter-period-search-predicate.ts
17838
- var PERIOD_SEARCH_PREFIXES = [
17839
- "gt",
17840
- "lt",
17841
- "ge",
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
- var PERIOD_START = "resource->'period'->>'start'";
17850
- var PERIOD_END = "resource->'period'->>'end'";
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 periodConstraintParamName(index) {
17869
- return `periodConstraint${index}`;
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 buildPeriodSearchPredicateSql(constraints) {
17872
- if (constraints.length === 0) {
17873
- return [];
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
- const fragments = constraints.map(
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 buildPeriodSearchPredicateParams(constraints) {
17882
- return constraints.map((c, i) => ({
17883
- name: periodConstraintParamName(i),
17884
- value: c.value
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
- // src/data/operations/data/encounter/encounter-search-by-date-operation.ts
17889
- var DEFAULT_LIMIT4 = 100;
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 lines = [
17895
- "SELECT resource_id AS id, resource",
17896
- "FROM resources",
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
- if (datetime === "" || Number.isNaN(Date.parse(datetime))) {
18092
- return { error: `Invalid datetime in ?date=${trimmed}.` };
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 out;
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 patientId = singleStringQueryParam2(req, "patient");
18106
- const participantRef = singleStringQueryParam2(req, "participant");
18107
- const parsed = parseEncounterDateConstraints(req);
18108
- if (isError2(parsed)) {
18109
- return sendInvalidSearch4002(res, parsed.error);
18110
- }
18111
- const periodConstraints = parsed;
18112
- if (patientId !== void 0 && participantRef !== void 0) {
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
- "?patient= and ?participant= cannot be combined on the same request."
18131
+ buildUnknownParamDiagnostics2([...new Set(unknownCodes)])
18116
18132
  );
18117
18133
  }
18118
- if (participantRef !== void 0) {
18119
- if (parseTypedReference(participantRef) === void 0) {
18120
- return sendInvalidSearch4002(
18121
- res,
18122
- `?participant must be a typed reference like "Practitioner/<id>"; got "${participantRef}".`
18123
- );
18124
- }
18125
- const ctx = req.openhiContext;
18126
- try {
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
- if (periodConstraints.length > 0) {
18161
- const ctx = req.openhiContext;
18162
- try {
18163
- const result = await searchEncountersByDateOperation({
18164
- context: ctx,
18165
- periodConstraints
18166
- });
18167
- const bundle = buildSearchsetBundle(BASE_PATH.ENCOUNTER, result.entries);
18168
- return res.json(bundle);
18169
- } catch (err) {
18170
- return sendOperationOutcome500(
18171
- res,
18172
- err,
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
- async function listObservationsRoute(req, res) {
28899
- return handleListRoute({
28900
- req,
28901
- res,
28902
- basePath: BASE_PATH.OBSERVATION,
28903
- listOperation: listObservationsOperation,
28904
- errorLogContext: "GET /Observation list error:"
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
- // src/data/operations/data/observation/observation-update-operation.ts
28909
- async function updateObservationOperation(params) {
28910
- const { context, id, body, tableName } = params;
28911
- const { tenantId, workspaceId, date, actorId, actorName } = context;
28912
- const service = getDynamoDataService(tableName);
28913
- return updateDataEntityById(
28914
- service.entities.observation,
28915
- tenantId,
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
- // src/data/rest-api/routes/data/observation/observation-update-route.ts
28933
- async function updateObservationRoute(req, res) {
28934
- const bodyResult = requireJsonBodyAs(req, res);
28935
- if ("errorResponse" in bodyResult) return bodyResult.errorResponse;
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/search/operations/generic-search-operation.ts
30208
- var DEFAULT_LIMIT7 = 100;
30209
- function buildGenericSearchSql(opts) {
30210
- const lines = [
30211
- "SELECT resource_id AS id, resource",
30212
- "FROM resources",
30213
- "WHERE tenant_id = :tenantId",
30214
- " AND workspace_id = :workspaceId",
30215
- " AND resource_type = :resourceType",
30216
- " AND deleted_at IS NULL"
30217
- ];
30218
- if (opts.combinedSql.length > 0) {
30219
- lines.push(` AND (${opts.combinedSql})`);
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
- async function genericSearchOperation(params) {
30226
- const { resourceType, tenantId, workspaceId, query, resolver } = params;
30227
- const runner = params.runner ?? getDefaultPostgresQueryRunner();
30228
- const limit = params.limit ?? DEFAULT_LIMIT7;
30229
- const registeredParams = resolver(resourceType, tenantId);
30230
- const context = { tenantId, workspaceId, resourceType };
30231
- const combined = combineSearchPredicates({
30232
- query,
30233
- registeredParams,
30234
- context
30235
- });
30236
- const sql = buildGenericSearchSql({ combinedSql: combined.sql });
30237
- const queryParams = [
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/search/registry/patient-search-parameters.ts
30253
- var PATIENT_SEARCH_PARAMETERS = [
30254
- { code: "gender", type: "token", jsonbPath: "$.gender" },
30255
- { code: "identifier", type: "token", jsonbPath: "$.identifier[*].value" },
30256
- { code: "birthdate", type: "date", jsonbPath: "$.birthDate" },
30257
- {
30258
- code: "name",
30259
- type: "string",
30260
- jsonbPath: "$.name[*].text",
30261
- modifiers: ["exact", "contains", "missing", "not"]
30262
- },
30263
- {
30264
- code: "family",
30265
- type: "string",
30266
- jsonbPath: "$.name[*].family",
30267
- modifiers: ["exact", "contains", "missing", "not"]
30268
- },
30269
- {
30270
- code: "given",
30271
- type: "string",
30272
- jsonbPath: "$.name[*].given",
30273
- modifiers: ["exact", "contains", "missing", "not"]
30274
- },
30275
- { code: "active", type: "token", jsonbPath: "$.active" },
30276
- { code: "deceased", type: "token", jsonbPath: "$.deceasedBoolean" },
30277
- {
30278
- code: "general-practitioner",
30279
- type: "reference",
30280
- jsonbPath: "$.generalPractitioner[*]"
30281
- },
30282
- {
30283
- code: "organization",
30284
- type: "reference",
30285
- jsonbPath: "$.managingOrganization"
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/search/registry/resolver.ts
30290
- var STATIC_SEARCH_PARAMETER_MAP = {
30291
- Patient: PATIENT_SEARCH_PARAMETERS
30292
- };
30293
- var defaultSearchParameterResolver = (resourceType, _tenantId) => STATIC_SEARCH_PARAMETER_MAP[resourceType] ?? [];
30294
- function getRegisteredSearchParameters(resourceType) {
30295
- return STATIC_SEARCH_PARAMETER_MAP[resourceType] ?? [];
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 stripModifier(key) {
29984
+ function stripModifier4(key) {
30301
29985
  const idx = key.indexOf(":");
30302
29986
  return idx === -1 ? key : key.slice(0, idx);
30303
29987
  }
30304
- function isResultParameter(key) {
29988
+ function isResultParameter4(key) {
30305
29989
  return key.startsWith("_");
30306
29990
  }
30307
- function sendInvalidSearch4003(res, diagnostics) {
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 extractSearchParamKeys(query) {
29997
+ function extractSearchParamKeys4(query) {
30314
29998
  const out = [];
30315
29999
  for (const rawKey of Object.keys(query)) {
30316
- if (isResultParameter(rawKey)) {
30000
+ if (isResultParameter4(rawKey)) {
30317
30001
  continue;
30318
30002
  }
30319
- out.push({ rawKey, code: stripModifier(rawKey) });
30003
+ out.push({ rawKey, code: stripModifier4(rawKey) });
30320
30004
  }
30321
30005
  return out;
30322
30006
  }
30323
- function buildUnknownParamDiagnostics(unknownCodes) {
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 findMalformedReference(query, searchParamKeys) {
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 = extractSearchParamKeys(
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 sendInvalidSearch4003(
30055
+ return sendInvalidSearch4004(
30372
30056
  res,
30373
- buildUnknownParamDiagnostics([...new Set(unknownCodes)])
30057
+ buildUnknownParamDiagnostics4([...new Set(unknownCodes)])
30374
30058
  );
30375
30059
  }
30376
- const malformedRef = findMalformedReference(
30060
+ const malformedRef = findMalformedReference4(
30377
30061
  req.query,
30378
30062
  searchParamKeys
30379
30063
  );
30380
30064
  if (malformedRef !== void 0) {
30381
- return sendInvalidSearch4003(
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
- async function listProceduresRoute(req, res) {
31756
- return handleListRoute({
31757
- req,
31758
- res,
31759
- basePath: BASE_PATH.PROCEDURE,
31760
- listOperation: listProceduresOperation,
31761
- errorLogContext: "GET /Procedure list error:"
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 DEFAULT_LIMIT8 = 100;
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 ?? DEFAULT_LIMIT8;
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 singleStringQueryParam3(req, name) {
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 sendInvalidSearch4004(res, diagnostics) {
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 = singleStringQueryParam3(req, "actor");
34055
+ const actorRef = singleStringQueryParam(req, "actor");
34279
34056
  if (actorRef !== void 0) {
34280
34057
  if (parseTypedReference(actorRef) === void 0) {
34281
- return sendInvalidSearch4004(
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 DEFAULT_LIMIT9 = 100;
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 ?? DEFAULT_LIMIT9;
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 DEFAULT_LIMIT10 = 100;
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 ?? DEFAULT_LIMIT10;
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 singleStringQueryParam4(req, name) {
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 sendInvalidSearch4005(res, diagnostics) {
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 = singleStringQueryParam4(req, "owner");
38087
- const requesterRef = singleStringQueryParam4(req, "requester");
37863
+ const ownerRef = singleStringQueryParam2(req, "owner");
37864
+ const requesterRef = singleStringQueryParam2(req, "requester");
38088
37865
  if (ownerRef !== void 0 && requesterRef !== void 0) {
38089
- return sendInvalidSearch4005(
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 sendInvalidSearch4005(
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 sendInvalidSearch4005(
37896
+ return sendInvalidSearch4007(
38120
37897
  res,
38121
37898
  `?requester must be a typed reference like "Practitioner/<id>"; got "${requesterRef}".`
38122
37899
  );