@openhi/constructs 0.0.177 → 0.0.179

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.
Files changed (54) hide show
  1. package/lib/{chunk-Z4PZSLYY.mjs → chunk-3M4QTQH6.mjs} +2 -2
  2. package/lib/{chunk-JUSVETWK.mjs → chunk-4LQR32D2.mjs} +38 -40
  3. package/lib/{chunk-JUSVETWK.mjs.map → chunk-4LQR32D2.mjs.map} +1 -1
  4. package/lib/{chunk-XNUCKVSE.mjs → chunk-7GMTHOYF.mjs} +2 -2
  5. package/lib/{chunk-E2OWEBBH.mjs → chunk-DIVYB6GD.mjs} +18 -4
  6. package/lib/chunk-DIVYB6GD.mjs.map +1 -0
  7. package/lib/chunk-F2LY4TEI.mjs +272 -0
  8. package/lib/chunk-F2LY4TEI.mjs.map +1 -0
  9. package/lib/{chunk-GG2WD6TA.mjs → chunk-JJ3AQ6G5.mjs} +9 -3
  10. package/lib/{chunk-GG2WD6TA.mjs.map → chunk-JJ3AQ6G5.mjs.map} +1 -1
  11. package/lib/{chunk-EBB4RNUG.mjs → chunk-PIQISEGW.mjs} +2 -2
  12. package/lib/{chunk-FDBBTNCI.mjs → chunk-Q4KQD2NB.mjs} +117 -5
  13. package/lib/chunk-Q4KQD2NB.mjs.map +1 -0
  14. package/lib/{chunk-Y4RGUAM2.mjs → chunk-V6KLFEHC.mjs} +105 -34
  15. package/lib/chunk-V6KLFEHC.mjs.map +1 -0
  16. package/lib/chunk-VQY57NOV.mjs +60 -0
  17. package/lib/chunk-VQY57NOV.mjs.map +1 -0
  18. package/lib/counter-maintenance.handler.mjs +4 -4
  19. package/lib/counter-reconciliation.handler.js +2 -2
  20. package/lib/counter-reconciliation.handler.js.map +1 -1
  21. package/lib/counter-reconciliation.handler.mjs +9 -267
  22. package/lib/counter-reconciliation.handler.mjs.map +1 -1
  23. package/lib/index.d.mts +117 -2
  24. package/lib/index.d.ts +117 -2
  25. package/lib/index.js +6454 -6243
  26. package/lib/index.js.map +1 -1
  27. package/lib/index.mjs +106 -4
  28. package/lib/index.mjs.map +1 -1
  29. package/lib/pre-token-generation.handler.js +28 -19
  30. package/lib/pre-token-generation.handler.js.map +1 -1
  31. package/lib/pre-token-generation.handler.mjs +4 -5
  32. package/lib/pre-token-generation.handler.mjs.map +1 -1
  33. package/lib/provision-default-workspace.handler.js +22 -19
  34. package/lib/provision-default-workspace.handler.js.map +1 -1
  35. package/lib/provision-default-workspace.handler.mjs +3 -4
  36. package/lib/provision-default-workspace.handler.mjs.map +1 -1
  37. package/lib/rest-api-lambda.handler.js +400 -214
  38. package/lib/rest-api-lambda.handler.js.map +1 -1
  39. package/lib/rest-api-lambda.handler.mjs +243 -171
  40. package/lib/rest-api-lambda.handler.mjs.map +1 -1
  41. package/lib/seed-demo-data.handler.d.mts +19 -0
  42. package/lib/seed-demo-data.handler.d.ts +19 -0
  43. package/lib/seed-demo-data.handler.js +805 -159
  44. package/lib/seed-demo-data.handler.js.map +1 -1
  45. package/lib/seed-demo-data.handler.mjs +8 -4
  46. package/package.json +3 -3
  47. package/lib/chunk-6HGSR3TG.mjs +0 -123
  48. package/lib/chunk-6HGSR3TG.mjs.map +0 -1
  49. package/lib/chunk-E2OWEBBH.mjs.map +0 -1
  50. package/lib/chunk-FDBBTNCI.mjs.map +0 -1
  51. package/lib/chunk-Y4RGUAM2.mjs.map +0 -1
  52. /package/lib/{chunk-Z4PZSLYY.mjs.map → chunk-3M4QTQH6.mjs.map} +0 -0
  53. /package/lib/{chunk-XNUCKVSE.mjs.map → chunk-7GMTHOYF.mjs.map} +0 -0
  54. /package/lib/{chunk-EBB4RNUG.mjs.map → chunk-PIQISEGW.mjs.map} +0 -0
@@ -3578,7 +3578,7 @@ async function deleteDataEntityById(entity, tenantId, workspaceId, id) {
3578
3578
  }
3579
3579
  var BATCH_GET_MAX_ATTEMPTS = 3;
3580
3580
  var BATCH_GET_BASE_BACKOFF_MS = 50;
3581
- async function batchGetWithRetry(entity, keys) {
3581
+ async function batchGetWithRetry(entity, keys, options) {
3582
3582
  if (keys.length === 0) return [];
3583
3583
  const collected = [];
3584
3584
  let pending = keys;
@@ -3590,7 +3590,7 @@ async function batchGetWithRetry(entity, keys) {
3590
3590
  );
3591
3591
  }
3592
3592
  attempt++;
3593
- const result = await entity.get(pending).go();
3593
+ const result = await entity.get(pending).go(options?.consistent ? { consistent: true } : void 0);
3594
3594
  collected.push(...result.data);
3595
3595
  const unprocessed = result.unprocessed ?? [];
3596
3596
  if (unprocessed.length === 0) break;
@@ -7153,9 +7153,15 @@ async function listRoleAssignmentsOperation(params) {
7153
7153
  )
7154
7154
  );
7155
7155
  return dispatchListMode(mode, shardResults, {
7156
+ // Strongly-consistent BatchGet on the base table so a role change is
7157
+ // reflected immediately by the read-back the admin-console dropdowns
7158
+ // issue right after their PUT/POST. Without it the eventually-consistent
7159
+ // hydration can return the *previous* role, making the dropdown appear
7160
+ // to revert (#1347).
7156
7161
  hydrate: (orderedIds) => batchGetWithRetry(
7157
7162
  service.entities.roleAssignment,
7158
- orderedIds.map((id) => ({ tenantId, id, sk: SK8 }))
7163
+ orderedIds.map((id) => ({ tenantId, id, sk: SK8 })),
7164
+ { consistent: true }
7159
7165
  ),
7160
7166
  getId: (item) => item.id,
7161
7167
  buildEntry: (id, item) => ({
@@ -7543,6 +7549,76 @@ async function listTenantsRoute(req, res) {
7543
7549
  });
7544
7550
  }
7545
7551
 
7552
+ // src/data/operations/control/roleassignment/roleassignment-list-by-user-operation.ts
7553
+ function buildSkPrefix(mode) {
7554
+ switch (mode) {
7555
+ case "tenant":
7556
+ return "ROLEASSIGNMENT#TENANT#";
7557
+ case "workspace":
7558
+ case "workspaceInTenant":
7559
+ return "ROLEASSIGNMENT#WORKSPACE#";
7560
+ case "all":
7561
+ default:
7562
+ return "ROLEASSIGNMENT#";
7563
+ }
7564
+ }
7565
+ async function roleAssignmentListByUserOperation(params) {
7566
+ const {
7567
+ userId,
7568
+ mode = "all",
7569
+ tenantId,
7570
+ workspaceId,
7571
+ cursor = null,
7572
+ limit,
7573
+ order,
7574
+ consistent,
7575
+ tableName
7576
+ } = params;
7577
+ if (mode === "workspaceInTenant" && !tenantId) {
7578
+ throw new Error(
7579
+ 'roleAssignmentListByUserOperation: tenantId is required when mode === "workspaceInTenant"'
7580
+ );
7581
+ }
7582
+ const service = getDynamoControlService(tableName);
7583
+ const skPrefix = buildSkPrefix(mode);
7584
+ const goOptions = {
7585
+ cursor
7586
+ };
7587
+ if (limit !== void 0) {
7588
+ goOptions.limit = limit;
7589
+ }
7590
+ if (order !== void 0) {
7591
+ goOptions.order = order;
7592
+ }
7593
+ if (consistent) {
7594
+ goOptions.consistent = true;
7595
+ }
7596
+ const baseQuery = service.entities.roleAssignmentUserProjection.query.record({ userId }).begins({ sk: skPrefix });
7597
+ const filteredQuery = mode === "workspaceInTenant" ? baseQuery.where((attr, op) => {
7598
+ const tenantClause = op.eq(attr.tenantId, tenantId);
7599
+ if (workspaceId === void 0 || workspaceId.length === 0) {
7600
+ return tenantClause;
7601
+ }
7602
+ return `${tenantClause} AND ${op.eq(attr.workspaceId, workspaceId)}`;
7603
+ }) : baseQuery;
7604
+ const result = await filteredQuery.go(goOptions);
7605
+ const items = (result.data ?? []).map((row) => ({
7606
+ userId: row.userId,
7607
+ sk: row.sk,
7608
+ tenantId: row.tenantId,
7609
+ workspaceId: row.workspaceId,
7610
+ roleId: row.roleId,
7611
+ roleAssignmentId: row.roleAssignmentId,
7612
+ summary: row.summary,
7613
+ vid: row.vid,
7614
+ lastUpdated: row.lastUpdated,
7615
+ denormalizedTenantName: row.denormalizedTenantName,
7616
+ denormalizedUserName: row.denormalizedUserName,
7617
+ denormalizedRoleName: row.denormalizedRoleName
7618
+ }));
7619
+ return { items, cursor: result.cursor ?? null };
7620
+ }
7621
+
7546
7622
  // src/data/operations/control/workspace/workspace-list-operation.ts
7547
7623
  var SK10 = "CURRENT";
7548
7624
  function counterValue2(value) {
@@ -7626,18 +7702,6 @@ function extractDisplayName(user) {
7626
7702
  const composed = `${given} ${family}`.trim();
7627
7703
  return composed.length > 0 ? composed : null;
7628
7704
  }
7629
- function extractReferenceDisplay(resource, fieldName) {
7630
- const field = resource[fieldName];
7631
- if (!field || typeof field !== "object") {
7632
- return null;
7633
- }
7634
- const display = field.display;
7635
- if (typeof display !== "string") {
7636
- return null;
7637
- }
7638
- const trimmed = display.trim();
7639
- return trimmed.length > 0 ? trimmed : null;
7640
- }
7641
7705
  function ensureAccumulator(byUser, userId) {
7642
7706
  let acc = byUser.get(userId);
7643
7707
  if (!acc) {
@@ -7649,9 +7713,8 @@ function ensureAccumulator(byUser, userId) {
7649
7713
  async function tenantUsersOperation(params) {
7650
7714
  const { context, tableName } = params;
7651
7715
  const service = getDynamoControlService(tableName);
7652
- const [memberships, roleAssignments, workspaces] = await Promise.all([
7716
+ const [memberships, workspaces] = await Promise.all([
7653
7717
  listMembershipsOperation({ context, tableName }),
7654
- listRoleAssignmentsOperation({ context, tableName }),
7655
7718
  listWorkspacesOperation({ context, tableName })
7656
7719
  ]);
7657
7720
  const workspaceNames = /* @__PURE__ */ new Map();
@@ -7673,34 +7736,46 @@ async function tenantUsersOperation(params) {
7673
7736
  acc.workspaces.set(workspaceId, { workspaceId, role: null });
7674
7737
  }
7675
7738
  }
7676
- for (const entry of roleAssignments.entries) {
7677
- const resource = entry.resource;
7678
- const userId = extractReferenceSlug(resource, "user");
7679
- const roleId = extractReferenceSlug(resource, "role");
7680
- if (userId === void 0 || roleId === void 0) {
7681
- continue;
7682
- }
7739
+ const membershipUserIds = Array.from(byUser.keys());
7740
+ const projectionPages = await Promise.all(
7741
+ membershipUserIds.map(
7742
+ (userId) => roleAssignmentListByUserOperation({
7743
+ userId,
7744
+ mode: "all",
7745
+ consistent: true,
7746
+ tableName
7747
+ })
7748
+ )
7749
+ );
7750
+ projectionPages.forEach((page, index) => {
7751
+ const userId = membershipUserIds[index];
7683
7752
  const acc = ensureAccumulator(byUser, userId);
7684
- const role = {
7685
- roleId,
7686
- roleName: extractReferenceDisplay(resource, "role")
7687
- };
7688
- const workspaceId = extractReferenceSlug(resource, "workspace");
7689
- if (workspaceId === void 0) {
7690
- acc.tenantRole = role;
7691
- } else {
7692
- const existing = acc.workspaces.get(workspaceId);
7693
- if (existing) {
7694
- existing.role = role;
7753
+ for (const row of page.items) {
7754
+ if (row.tenantId !== context.tenantId) {
7755
+ continue;
7756
+ }
7757
+ const role = {
7758
+ roleId: row.roleId,
7759
+ roleName: row.denormalizedRoleName ?? null
7760
+ };
7761
+ const workspaceId = row.workspaceId;
7762
+ if (workspaceId === void 0 || workspaceId.length === 0) {
7763
+ acc.tenantRole = role;
7695
7764
  } else {
7696
- acc.workspaces.set(workspaceId, { workspaceId, role });
7765
+ const existing = acc.workspaces.get(workspaceId);
7766
+ if (existing) {
7767
+ existing.role = role;
7768
+ } else {
7769
+ acc.workspaces.set(workspaceId, { workspaceId, role });
7770
+ }
7697
7771
  }
7698
7772
  }
7699
- }
7773
+ });
7700
7774
  const userIds = Array.from(byUser.keys());
7701
7775
  const userRows = userIds.length === 0 ? [] : await batchGetWithRetry(
7702
7776
  service.entities.user,
7703
- userIds.map((id) => ({ id, sk: USER_SK }))
7777
+ userIds.map((id) => ({ id, sk: USER_SK })),
7778
+ { consistent: true }
7704
7779
  );
7705
7780
  const usersById = /* @__PURE__ */ new Map();
7706
7781
  for (const row of userRows) {
@@ -7847,8 +7922,240 @@ router6.delete("/:id", deleteTenantRoute);
7847
7922
  // src/data/rest-api/routes/control/user/user.ts
7848
7923
  var import_express7 = __toESM(require("express"));
7849
7924
 
7850
- // src/data/operations/control/user/user-create-operation.ts
7925
+ // src/data/operations/control/user/user-admin-set-context-operation.ts
7851
7926
  var import_types18 = require("@openhi/types");
7927
+
7928
+ // src/data/operations/control/user/user-get-by-id-operation.ts
7929
+ async function getUserByIdOperation(params) {
7930
+ const { id, tableName } = params;
7931
+ const service = getDynamoControlService(tableName);
7932
+ const response = await service.entities.user.get({ id, sk: "CURRENT" }).go();
7933
+ const item = response.data;
7934
+ if (!item) {
7935
+ throw new NotFoundError(`User not found: ${id}`);
7936
+ }
7937
+ const parsedResource = JSON.parse(item.resource);
7938
+ return {
7939
+ id,
7940
+ resource: { resourceType: "User", id, ...parsedResource }
7941
+ };
7942
+ }
7943
+
7944
+ // src/data/operations/fhir-reference.ts
7945
+ function idFromReference(reference, prefix) {
7946
+ if (!reference || !reference.startsWith(prefix)) {
7947
+ return void 0;
7948
+ }
7949
+ const id = reference.slice(prefix.length);
7950
+ return id.length > 0 ? id : void 0;
7951
+ }
7952
+
7953
+ // src/data/operations/control/membership/membership-list-by-user-operation.ts
7954
+ function buildSkPrefix2(mode, tenantId) {
7955
+ switch (mode) {
7956
+ case "tenant":
7957
+ return "MEMBERSHIP#TENANT#";
7958
+ case "workspace":
7959
+ return "MEMBERSHIP#WORKSPACE#";
7960
+ case "workspaceInTenant":
7961
+ return `MEMBERSHIP#WORKSPACE#TID#${tenantId}#`;
7962
+ case "all":
7963
+ default:
7964
+ return "MEMBERSHIP#";
7965
+ }
7966
+ }
7967
+ async function membershipListByUserOperation(params) {
7968
+ const {
7969
+ userId,
7970
+ mode = "all",
7971
+ tenantId,
7972
+ cursor = null,
7973
+ limit,
7974
+ order,
7975
+ tableName
7976
+ } = params;
7977
+ if (mode === "workspaceInTenant" && !tenantId) {
7978
+ throw new Error(
7979
+ 'membershipListByUserOperation: tenantId is required when mode === "workspaceInTenant"'
7980
+ );
7981
+ }
7982
+ const service = getDynamoControlService(tableName);
7983
+ const skPrefix = buildSkPrefix2(mode, tenantId);
7984
+ const goOptions = {
7985
+ cursor
7986
+ };
7987
+ if (limit !== void 0) {
7988
+ goOptions.limit = limit;
7989
+ }
7990
+ if (order !== void 0) {
7991
+ goOptions.order = order;
7992
+ }
7993
+ const result = await service.entities.membershipUserProjection.query.record({ userId }).begins({ sk: skPrefix }).go(goOptions);
7994
+ const items = (result.data ?? []).map(
7995
+ (row) => ({
7996
+ userId: row.userId,
7997
+ sk: row.sk,
7998
+ tenantId: row.tenantId,
7999
+ workspaceId: row.workspaceId,
8000
+ membershipId: row.membershipId,
8001
+ summary: row.summary,
8002
+ vid: row.vid,
8003
+ lastUpdated: row.lastUpdated,
8004
+ denormalizedTenantName: row.denormalizedTenantName,
8005
+ denormalizedUserName: row.denormalizedUserName,
8006
+ denormalizedWorkspaceName: row.denormalizedWorkspaceName
8007
+ })
8008
+ );
8009
+ return { items, cursor: result.cursor ?? null };
8010
+ }
8011
+
8012
+ // src/data/operations/control/user/user-admin-set-context-operation.ts
8013
+ var SK11 = "CURRENT";
8014
+ async function adminSetUserContextOperation(params) {
8015
+ const {
8016
+ context,
8017
+ targetUserId,
8018
+ tenantReference,
8019
+ workspaceReference,
8020
+ tableName
8021
+ } = params;
8022
+ const tenantId = idFromReference(tenantReference, "Tenant/");
8023
+ if (!tenantId) {
8024
+ throw new ValidationError(
8025
+ "tenant.reference must be a 'Tenant/<id>' reference."
8026
+ );
8027
+ }
8028
+ const workspaceId = idFromReference(workspaceReference, "Workspace/");
8029
+ if (!workspaceId) {
8030
+ throw new ValidationError(
8031
+ "workspace.reference must be a 'Workspace/<id>' reference."
8032
+ );
8033
+ }
8034
+ const target = await getUserByIdOperation({
8035
+ context,
8036
+ id: targetUserId,
8037
+ tableName
8038
+ });
8039
+ const projection = await membershipListByUserOperation({
8040
+ userId: targetUserId,
8041
+ mode: "workspaceInTenant",
8042
+ tenantId,
8043
+ tableName
8044
+ });
8045
+ const hasMembership = projection.items.some(
8046
+ (row) => row.workspaceId === workspaceId
8047
+ );
8048
+ if (!hasMembership) {
8049
+ throw new ForbiddenError(
8050
+ `User is not a member of Workspace/${workspaceId} in Tenant/${tenantId}.`
8051
+ );
8052
+ }
8053
+ const updatedResource = {
8054
+ ...target.resource,
8055
+ resourceType: "User",
8056
+ id: targetUserId,
8057
+ currentTenant: { reference: `Tenant/${tenantId}` },
8058
+ currentWorkspace: { reference: `Workspace/${workspaceId}` }
8059
+ };
8060
+ const lastUpdated = (params.now ? params.now() : /* @__PURE__ */ new Date()).toISOString();
8061
+ const vid = `${Date.now()}`;
8062
+ const summary = JSON.stringify(
8063
+ (0, import_types18.extractSummary)(updatedResource)
8064
+ );
8065
+ const service = getDynamoControlService(tableName);
8066
+ await service.entities.user.patch({ id: targetUserId, sk: SK11 }).set({
8067
+ resource: JSON.stringify(updatedResource),
8068
+ summary,
8069
+ vid,
8070
+ lastUpdated
8071
+ }).go();
8072
+ return {
8073
+ id: targetUserId,
8074
+ resource: updatedResource,
8075
+ meta: { lastUpdated, versionId: vid }
8076
+ };
8077
+ }
8078
+
8079
+ // src/data/rest-api/routes/control/user/user-admin-set-context-route.ts
8080
+ async function userAdminSetContextRoute(req, res) {
8081
+ const ctx = req.openhiContext;
8082
+ if (!ctx) {
8083
+ return res.status(403).json({
8084
+ resourceType: "OperationOutcome",
8085
+ issue: [
8086
+ {
8087
+ severity: "error",
8088
+ code: "forbidden",
8089
+ diagnostics: "Missing or invalid OpenHI JWT claims (tenant, workspace, or audit context)."
8090
+ }
8091
+ ]
8092
+ });
8093
+ }
8094
+ const targetUserId = String(req.params.id);
8095
+ const bodyResult = requireJsonBody(req, res);
8096
+ if ("errorResponse" in bodyResult) {
8097
+ return bodyResult.errorResponse;
8098
+ }
8099
+ const body = bodyResult.body;
8100
+ const tenantReference = body.tenant?.reference;
8101
+ const workspaceReference = body.workspace?.reference;
8102
+ if (typeof tenantReference !== "string" || tenantReference === "") {
8103
+ return sendInvalid(
8104
+ res,
8105
+ "Body must include `tenant.reference` (e.g. 'Tenant/<id>')."
8106
+ );
8107
+ }
8108
+ if (typeof workspaceReference !== "string" || workspaceReference === "") {
8109
+ return sendInvalid(
8110
+ res,
8111
+ "Body must include `workspace.reference` (e.g. 'Workspace/<id>')."
8112
+ );
8113
+ }
8114
+ try {
8115
+ const result = await adminSetUserContextOperation({
8116
+ context: ctx,
8117
+ targetUserId,
8118
+ tenantReference,
8119
+ workspaceReference
8120
+ });
8121
+ res.setHeader("Cache-Control", "no-store");
8122
+ return res.status(200).json(result.resource);
8123
+ } catch (err) {
8124
+ if (err instanceof ValidationError) {
8125
+ return sendInvalid(res, err.message);
8126
+ }
8127
+ if (err instanceof ForbiddenError) {
8128
+ return res.status(403).json({
8129
+ resourceType: "OperationOutcome",
8130
+ issue: [
8131
+ { severity: "error", code: "forbidden", diagnostics: err.message }
8132
+ ]
8133
+ });
8134
+ }
8135
+ if (err instanceof NotFoundError) {
8136
+ return res.status(404).json({
8137
+ resourceType: "OperationOutcome",
8138
+ issue: [
8139
+ { severity: "error", code: "not-found", diagnostics: err.message }
8140
+ ]
8141
+ });
8142
+ }
8143
+ return sendOperationOutcome500(
8144
+ res,
8145
+ err,
8146
+ "POST /User/:id/$set-context error:"
8147
+ );
8148
+ }
8149
+ }
8150
+ function sendInvalid(res, diagnostics) {
8151
+ return res.status(400).json({
8152
+ resourceType: "OperationOutcome",
8153
+ issue: [{ severity: "error", code: "invalid", diagnostics }]
8154
+ });
8155
+ }
8156
+
8157
+ // src/data/operations/control/user/user-create-operation.ts
8158
+ var import_types19 = require("@openhi/types");
7852
8159
  var import_ulid5 = require("ulid");
7853
8160
  async function createUserOperation(params) {
7854
8161
  const { context, body, tableName } = params;
@@ -7858,7 +8165,7 @@ async function createUserOperation(params) {
7858
8165
  const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
7859
8166
  const vid = `1`;
7860
8167
  const resource = { resourceType: "User", id, ...parsedResource };
7861
- const summary = JSON.stringify((0, import_types18.extractSummary)(resource));
8168
+ const summary = JSON.stringify((0, import_types19.extractSummary)(resource));
7862
8169
  await service.entities.user.put({
7863
8170
  id,
7864
8171
  resource: JSON.stringify(resource),
@@ -7925,22 +8232,6 @@ async function deleteUserRoute(req, res) {
7925
8232
  }
7926
8233
  }
7927
8234
 
7928
- // src/data/operations/control/user/user-get-by-id-operation.ts
7929
- async function getUserByIdOperation(params) {
7930
- const { id, tableName } = params;
7931
- const service = getDynamoControlService(tableName);
7932
- const response = await service.entities.user.get({ id, sk: "CURRENT" }).go();
7933
- const item = response.data;
7934
- if (!item) {
7935
- throw new NotFoundError(`User not found: ${id}`);
7936
- }
7937
- const parsedResource = JSON.parse(item.resource);
7938
- return {
7939
- id,
7940
- resource: { resourceType: "User", id, ...parsedResource }
7941
- };
7942
- }
7943
-
7944
8235
  // src/data/rest-api/routes/control/user/user-get-by-id-route.ts
7945
8236
  async function getUserByIdRoute(req, res) {
7946
8237
  const id = String(req.params.id);
@@ -8291,65 +8582,6 @@ async function listUserConfigurationsRoute(req, res) {
8291
8582
  }
8292
8583
  }
8293
8584
 
8294
- // src/data/operations/control/membership/membership-list-by-user-operation.ts
8295
- function buildSkPrefix(mode, tenantId) {
8296
- switch (mode) {
8297
- case "tenant":
8298
- return "MEMBERSHIP#TENANT#";
8299
- case "workspace":
8300
- return "MEMBERSHIP#WORKSPACE#";
8301
- case "workspaceInTenant":
8302
- return `MEMBERSHIP#WORKSPACE#TID#${tenantId}#`;
8303
- case "all":
8304
- default:
8305
- return "MEMBERSHIP#";
8306
- }
8307
- }
8308
- async function membershipListByUserOperation(params) {
8309
- const {
8310
- userId,
8311
- mode = "all",
8312
- tenantId,
8313
- cursor = null,
8314
- limit,
8315
- order,
8316
- tableName
8317
- } = params;
8318
- if (mode === "workspaceInTenant" && !tenantId) {
8319
- throw new Error(
8320
- 'membershipListByUserOperation: tenantId is required when mode === "workspaceInTenant"'
8321
- );
8322
- }
8323
- const service = getDynamoControlService(tableName);
8324
- const skPrefix = buildSkPrefix(mode, tenantId);
8325
- const goOptions = {
8326
- cursor
8327
- };
8328
- if (limit !== void 0) {
8329
- goOptions.limit = limit;
8330
- }
8331
- if (order !== void 0) {
8332
- goOptions.order = order;
8333
- }
8334
- const result = await service.entities.membershipUserProjection.query.record({ userId }).begins({ sk: skPrefix }).go(goOptions);
8335
- const items = (result.data ?? []).map(
8336
- (row) => ({
8337
- userId: row.userId,
8338
- sk: row.sk,
8339
- tenantId: row.tenantId,
8340
- workspaceId: row.workspaceId,
8341
- membershipId: row.membershipId,
8342
- summary: row.summary,
8343
- vid: row.vid,
8344
- lastUpdated: row.lastUpdated,
8345
- denormalizedTenantName: row.denormalizedTenantName,
8346
- denormalizedUserName: row.denormalizedUserName,
8347
- denormalizedWorkspaceName: row.denormalizedWorkspaceName
8348
- })
8349
- );
8350
- return { items, cursor: result.cursor ?? null };
8351
- }
8352
-
8353
8585
  // src/data/operations/control/membership/membership-count-by-user-operation.ts
8354
8586
  async function countMembershipsByUserOperation(params) {
8355
8587
  const { userId, mode = "all", tenantId, tableName } = params;
@@ -8359,7 +8591,7 @@ async function countMembershipsByUserOperation(params) {
8359
8591
  );
8360
8592
  }
8361
8593
  const service = getDynamoControlService(tableName);
8362
- const skPrefix = buildSkPrefix(mode, tenantId);
8594
+ const skPrefix = buildSkPrefix2(mode, tenantId);
8363
8595
  const result = await service.entities.membershipUserProjection.query.record({ userId }).begins({ sk: skPrefix }).go({ pages: "all", attributes: ["membershipId"] });
8364
8596
  return (result.data ?? []).length;
8365
8597
  }
@@ -8512,72 +8744,6 @@ async function listUserMembershipsRoute(req, res) {
8512
8744
  }
8513
8745
  }
8514
8746
 
8515
- // src/data/operations/control/roleassignment/roleassignment-list-by-user-operation.ts
8516
- function buildSkPrefix2(mode) {
8517
- switch (mode) {
8518
- case "tenant":
8519
- return "ROLEASSIGNMENT#TENANT#";
8520
- case "workspace":
8521
- case "workspaceInTenant":
8522
- return "ROLEASSIGNMENT#WORKSPACE#";
8523
- case "all":
8524
- default:
8525
- return "ROLEASSIGNMENT#";
8526
- }
8527
- }
8528
- async function roleAssignmentListByUserOperation(params) {
8529
- const {
8530
- userId,
8531
- mode = "all",
8532
- tenantId,
8533
- workspaceId,
8534
- cursor = null,
8535
- limit,
8536
- order,
8537
- tableName
8538
- } = params;
8539
- if (mode === "workspaceInTenant" && !tenantId) {
8540
- throw new Error(
8541
- 'roleAssignmentListByUserOperation: tenantId is required when mode === "workspaceInTenant"'
8542
- );
8543
- }
8544
- const service = getDynamoControlService(tableName);
8545
- const skPrefix = buildSkPrefix2(mode);
8546
- const goOptions = {
8547
- cursor
8548
- };
8549
- if (limit !== void 0) {
8550
- goOptions.limit = limit;
8551
- }
8552
- if (order !== void 0) {
8553
- goOptions.order = order;
8554
- }
8555
- const baseQuery = service.entities.roleAssignmentUserProjection.query.record({ userId }).begins({ sk: skPrefix });
8556
- const filteredQuery = mode === "workspaceInTenant" ? baseQuery.where((attr, op) => {
8557
- const tenantClause = op.eq(attr.tenantId, tenantId);
8558
- if (workspaceId === void 0 || workspaceId.length === 0) {
8559
- return tenantClause;
8560
- }
8561
- return `${tenantClause} AND ${op.eq(attr.workspaceId, workspaceId)}`;
8562
- }) : baseQuery;
8563
- const result = await filteredQuery.go(goOptions);
8564
- const items = (result.data ?? []).map((row) => ({
8565
- userId: row.userId,
8566
- sk: row.sk,
8567
- tenantId: row.tenantId,
8568
- workspaceId: row.workspaceId,
8569
- roleId: row.roleId,
8570
- roleAssignmentId: row.roleAssignmentId,
8571
- summary: row.summary,
8572
- vid: row.vid,
8573
- lastUpdated: row.lastUpdated,
8574
- denormalizedTenantName: row.denormalizedTenantName,
8575
- denormalizedUserName: row.denormalizedUserName,
8576
- denormalizedRoleName: row.denormalizedRoleName
8577
- }));
8578
- return { items, cursor: result.cursor ?? null };
8579
- }
8580
-
8581
8747
  // src/data/operations/control/roleassignment/roleassignment-list-by-workspace-operation.ts
8582
8748
  function buildSkPrefix3(roleId) {
8583
8749
  if (roleId === void 0 || roleId.length === 0) {
@@ -8714,7 +8880,7 @@ async function listUserRoleAssignmentsRoute(req, res) {
8714
8880
  }
8715
8881
 
8716
8882
  // src/data/operations/control/user/user-list-operation.ts
8717
- var SK11 = "CURRENT";
8883
+ var SK12 = "CURRENT";
8718
8884
  function counterValue3(value) {
8719
8885
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
8720
8886
  }
@@ -8730,7 +8896,7 @@ async function listUsersOperation(params) {
8730
8896
  return dispatchListMode(mode, shardResults, {
8731
8897
  hydrate: (orderedIds) => batchGetWithRetry(
8732
8898
  service.entities.user,
8733
- orderedIds.map((id) => ({ id, sk: SK11 }))
8899
+ orderedIds.map((id) => ({ id, sk: SK12 }))
8734
8900
  ),
8735
8901
  getId: (item) => item.id,
8736
8902
  // FULL mode (admin list default): read the ADR-028 counters off the
@@ -8774,7 +8940,7 @@ async function listUsersRoute(req, res) {
8774
8940
  }
8775
8941
 
8776
8942
  // src/data/operations/control/user/user-update-operation.ts
8777
- var import_types19 = require("@openhi/types");
8943
+ var import_types20 = require("@openhi/types");
8778
8944
  async function updateUserOperation(params) {
8779
8945
  const { context, id, body, tableName } = params;
8780
8946
  const service = getDynamoControlService(tableName);
@@ -8786,7 +8952,7 @@ async function updateUserOperation(params) {
8786
8952
  const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
8787
8953
  const vid = `${Date.now()}`;
8788
8954
  const resource = { resourceType: "User", id, ...parsedResource };
8789
- const summary = JSON.stringify((0, import_types19.extractSummary)(resource));
8955
+ const summary = JSON.stringify((0, import_types20.extractSummary)(resource));
8790
8956
  await service.entities.user.put({
8791
8957
  id,
8792
8958
  resource: JSON.stringify(resource),
@@ -8848,6 +9014,7 @@ router7.get("/:id", getUserByIdRoute);
8848
9014
  router7.post("/", createUserRoute);
8849
9015
  router7.put("/:id", updateUserRoute);
8850
9016
  router7.delete("/:id", deleteUserRoute);
9017
+ router7.post("/:id/$set-context", userAdminSetContextRoute);
8851
9018
  router7.get("/:id/Membership", listUserMembershipsRoute);
8852
9019
  router7.get("/:id/RoleAssignment", listUserRoleAssignmentsRoute);
8853
9020
  router7.get("/:id/Configuration", listUserConfigurationsRoute);
@@ -8893,7 +9060,7 @@ async function findUserBySubOperation(params) {
8893
9060
  }
8894
9061
 
8895
9062
  // src/data/operations/control/user/user-switch-tenant-workspace-operation.ts
8896
- var import_types20 = require("@openhi/types");
9063
+ var import_types21 = require("@openhi/types");
8897
9064
 
8898
9065
  // src/data/operations/control/user/user-resource-helpers.ts
8899
9066
  function parseUserResource(resource) {
@@ -8904,17 +9071,8 @@ function parseUserResource(resource) {
8904
9071
  }
8905
9072
  }
8906
9073
 
8907
- // src/data/operations/fhir-reference.ts
8908
- function idFromReference(reference, prefix) {
8909
- if (!reference || !reference.startsWith(prefix)) {
8910
- return void 0;
8911
- }
8912
- const id = reference.slice(prefix.length);
8913
- return id.length > 0 ? id : void 0;
8914
- }
8915
-
8916
9074
  // src/data/operations/control/user/user-switch-tenant-workspace-operation.ts
8917
- var SK12 = "CURRENT";
9075
+ var SK13 = "CURRENT";
8918
9076
  async function switchUserTenantWorkspaceOperation(params) {
8919
9077
  const { cognitoSub, tenantReference, workspaceReference, tableName } = params;
8920
9078
  const tenantId = idFromReference(tenantReference, "Tenant/");
@@ -8972,10 +9130,10 @@ async function switchUserTenantWorkspaceOperation(params) {
8972
9130
  const lastUpdated = (params.now ? params.now() : /* @__PURE__ */ new Date()).toISOString();
8973
9131
  const vid = `${Date.now()}`;
8974
9132
  const summary = JSON.stringify(
8975
- (0, import_types20.extractSummary)(updatedResource)
9133
+ (0, import_types21.extractSummary)(updatedResource)
8976
9134
  );
8977
9135
  const service = getDynamoControlService(tableName);
8978
- await service.entities.user.patch({ id: user.id, sk: SK12 }).set({
9136
+ await service.entities.user.patch({ id: user.id, sk: SK13 }).set({
8979
9137
  resource: JSON.stringify(updatedResource),
8980
9138
  summary,
8981
9139
  vid,
@@ -9330,13 +9488,13 @@ async function userSwitchRoute(req, res) {
9330
9488
  const tenantReference = body.tenant?.reference;
9331
9489
  const workspaceReference = body.workspace?.reference;
9332
9490
  if (typeof tenantReference !== "string" || tenantReference === "") {
9333
- return sendInvalid(
9491
+ return sendInvalid2(
9334
9492
  res,
9335
9493
  "Body must include `tenant.reference` (e.g. 'Tenant/<id>')."
9336
9494
  );
9337
9495
  }
9338
9496
  if (typeof workspaceReference !== "string" || workspaceReference === "") {
9339
- return sendInvalid(
9497
+ return sendInvalid2(
9340
9498
  res,
9341
9499
  "Body must include `workspace.reference` (e.g. 'Workspace/<id>')."
9342
9500
  );
@@ -9351,7 +9509,7 @@ async function userSwitchRoute(req, res) {
9351
9509
  return res.status(200).json(result.resource);
9352
9510
  } catch (err) {
9353
9511
  if (err instanceof ValidationError) {
9354
- return sendInvalid(res, err.message);
9512
+ return sendInvalid2(res, err.message);
9355
9513
  }
9356
9514
  if (err instanceof ForbiddenError) {
9357
9515
  return res.status(403).json({
@@ -9372,7 +9530,7 @@ async function userSwitchRoute(req, res) {
9372
9530
  return sendOperationOutcome500(res, err, "POST /User/$switch error:");
9373
9531
  }
9374
9532
  }
9375
- function sendInvalid(res, diagnostics) {
9533
+ function sendInvalid2(res, diagnostics) {
9376
9534
  return res.status(400).json({
9377
9535
  resourceType: "OperationOutcome",
9378
9536
  issue: [{ severity: "error", code: "invalid", diagnostics }]
@@ -9388,7 +9546,7 @@ router8.post("/$switch", userSwitchRoute);
9388
9546
  var import_express9 = __toESM(require("express"));
9389
9547
 
9390
9548
  // src/data/operations/control/workspace/workspace-create-operation.ts
9391
- var import_types21 = require("@openhi/types");
9549
+ var import_types22 = require("@openhi/types");
9392
9550
  var import_ulid6 = require("ulid");
9393
9551
 
9394
9552
  // src/data/operations/data/organization/organization-provision-for-workspace-operation.ts
@@ -9439,7 +9597,7 @@ async function createWorkspaceOperation(params) {
9439
9597
  const vid = lastUpdated.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36);
9440
9598
  const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
9441
9599
  const resource = { resourceType: "Workspace", id, ...parsedResource };
9442
- const summary = JSON.stringify((0, import_types21.extractSummary)(resource));
9600
+ const summary = JSON.stringify((0, import_types22.extractSummary)(resource));
9443
9601
  await service.entities.workspace.put({
9444
9602
  tenantId,
9445
9603
  id,
@@ -9803,7 +9961,7 @@ async function listWorkspacesRoute(req, res) {
9803
9961
  }
9804
9962
 
9805
9963
  // src/data/operations/control/workspace/workspace-update-operation.ts
9806
- var import_types22 = require("@openhi/types");
9964
+ var import_types23 = require("@openhi/types");
9807
9965
  async function updateWorkspaceOperation(params) {
9808
9966
  const { context, id, body, tableName } = params;
9809
9967
  const { tenantId } = context;
@@ -9822,7 +9980,7 @@ async function updateWorkspaceOperation(params) {
9822
9980
  resourceType: "Workspace",
9823
9981
  id
9824
9982
  };
9825
- const summary = JSON.stringify((0, import_types22.extractSummary)(updated));
9983
+ const summary = JSON.stringify((0, import_types23.extractSummary)(updated));
9826
9984
  await service.entities.workspace.patch({ tenantId, id, sk: "CURRENT" }).set({ resource: JSON.stringify(updated), summary, vid, lastUpdated }).go();
9827
9985
  return { id, resource: updated, meta: { lastUpdated, versionId: vid } };
9828
9986
  }
@@ -10964,6 +11122,16 @@ function jsonbPathToStringShape(jsonbPath) {
10964
11122
  if (arrayOfScalars) {
10965
11123
  return { kind: "array-of-scalars", field: arrayOfScalars[1] };
10966
11124
  }
11125
+ const arrayOfArrays = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(
11126
+ jsonbPath
11127
+ );
11128
+ if (arrayOfArrays) {
11129
+ return {
11130
+ kind: "array-of-arrays",
11131
+ field: arrayOfArrays[1],
11132
+ subfield: arrayOfArrays[2]
11133
+ };
11134
+ }
10967
11135
  const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
10968
11136
  jsonbPath
10969
11137
  );
@@ -10975,7 +11143,7 @@ function jsonbPathToStringShape(jsonbPath) {
10975
11143
  };
10976
11144
  }
10977
11145
  throw new Error(
10978
- `String predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
11146
+ `String predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield", "$.field[*].subfield[*]".`
10979
11147
  );
10980
11148
  }
10981
11149
  function escapeLikePattern(value) {
@@ -11008,6 +11176,13 @@ function buildIlikeExtractSql(shape, paramName) {
11008
11176
  `jsonb_array_elements(resource->'${shape.field}') AS s_obj(obj)`,
11009
11177
  `WHERE s_obj.obj->>'${shape.subfield}' ILIKE :${paramName})`
11010
11178
  ].join(" ");
11179
+ case "array-of-arrays":
11180
+ return [
11181
+ "EXISTS (SELECT 1 FROM",
11182
+ `jsonb_array_elements(resource->'${shape.field}') AS s_obj(obj),`,
11183
+ `jsonb_array_elements_text(s_obj.obj->'${shape.subfield}') AS s_elem(text_val)`,
11184
+ `WHERE s_elem.text_val ILIKE :${paramName})`
11185
+ ].join(" ");
11011
11186
  }
11012
11187
  }
11013
11188
  function emitStringPredicate(opts) {
@@ -11280,12 +11455,23 @@ async function genericSearchOperation(params) {
11280
11455
  ...combined.params
11281
11456
  ];
11282
11457
  const rows = await runner.query(sql, queryParams);
11283
- const entries = rows.map((row) => ({
11284
- id: row.id,
11285
- resource: { ...row.resource, id: row.id }
11286
- }));
11458
+ const entries = rows.map((row) => {
11459
+ const parsed = parseResourceColumn(row.resource);
11460
+ const bodyId = typeof parsed.id === "string" ? parsed.id : void 0;
11461
+ const id = bodyId ?? row.id;
11462
+ return {
11463
+ id,
11464
+ resource: { ...parsed, id }
11465
+ };
11466
+ });
11287
11467
  return { entries, total: entries.length };
11288
11468
  }
11469
+ function parseResourceColumn(raw) {
11470
+ if (typeof raw === "string") {
11471
+ return JSON.parse(raw);
11472
+ }
11473
+ return raw;
11474
+ }
11289
11475
 
11290
11476
  // src/data/search/registry/allergyintolerance-search-parameters.ts
11291
11477
  var ALLERGYINTOLERANCE_SEARCH_PARAMETERS = [
@@ -11886,7 +12072,7 @@ var PATIENT_SEARCH_PARAMETERS = [
11886
12072
  {
11887
12073
  code: "given",
11888
12074
  type: "string",
11889
- jsonbPath: "$.name[*].given",
12075
+ jsonbPath: "$.name[*].given[*]",
11890
12076
  modifiers: ["exact", "contains", "missing", "not"]
11891
12077
  },
11892
12078
  { code: "active", type: "token", jsonbPath: "$.active" },
@@ -11936,7 +12122,7 @@ var PRACTITIONER_SEARCH_PARAMETERS = [
11936
12122
  {
11937
12123
  code: "given",
11938
12124
  type: "string",
11939
- jsonbPath: "$.name[*].given",
12125
+ jsonbPath: "$.name[*].given[*]",
11940
12126
  modifiers: ["exact", "contains", "missing", "not"]
11941
12127
  },
11942
12128
  { code: "active", type: "token", jsonbPath: "$.active" },