@openhi/constructs 0.0.135 → 0.0.137

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