@openhi/constructs 0.0.160 → 0.0.161

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 (121) hide show
  1. package/lib/{chunk-HQ67J7BP.mjs → chunk-5S6VFBLT.mjs} +12 -70
  2. package/lib/chunk-5S6VFBLT.mjs.map +1 -0
  3. package/lib/{chunk-MVQWAIMC.mjs → chunk-6BB4CRSS.mjs} +3 -312
  4. package/lib/chunk-6BB4CRSS.mjs.map +1 -0
  5. package/lib/{chunk-WPCBVDFZ.mjs → chunk-76UM2LQ5.mjs} +2 -2
  6. package/lib/{chunk-QFHYTCVY.mjs → chunk-7TRO2STL.mjs} +7 -7
  7. package/lib/chunk-BUAYVN3C.mjs +87 -0
  8. package/lib/chunk-BUAYVN3C.mjs.map +1 -0
  9. package/lib/{chunk-23PUSHBV.mjs → chunk-D2Y6DDOC.mjs} +2 -2
  10. package/lib/chunk-DWSWCUZR.mjs +123 -0
  11. package/lib/chunk-DWSWCUZR.mjs.map +1 -0
  12. package/lib/{chunk-VZCPGQXA.mjs → chunk-EUIP2U5F.mjs} +69 -1
  13. package/lib/{chunk-VZCPGQXA.mjs.map → chunk-EUIP2U5F.mjs.map} +1 -1
  14. package/lib/chunk-GJTPXJKD.mjs +46 -0
  15. package/lib/chunk-GJTPXJKD.mjs.map +1 -0
  16. package/lib/chunk-I6LUPJUY.mjs +61 -0
  17. package/lib/chunk-I6LUPJUY.mjs.map +1 -0
  18. package/lib/{chunk-KR2Y2CVQ.mjs → chunk-KA3OMP3X.mjs} +2 -2
  19. package/lib/{chunk-ZM4GDHHC.mjs → chunk-KMEWULMX.mjs} +51 -3
  20. package/lib/chunk-KMEWULMX.mjs.map +1 -0
  21. package/lib/chunk-LKKLO66E.mjs +25 -0
  22. package/lib/chunk-LKKLO66E.mjs.map +1 -0
  23. package/lib/{chunk-CFJDATDK.mjs → chunk-MLFMW5IF.mjs} +43 -9
  24. package/lib/chunk-MLFMW5IF.mjs.map +1 -0
  25. package/lib/chunk-O5VQWB6U.mjs +315 -0
  26. package/lib/chunk-O5VQWB6U.mjs.map +1 -0
  27. package/lib/{chunk-7BQHLC7U.mjs → chunk-P3CTZWC2.mjs} +8 -40
  28. package/lib/chunk-P3CTZWC2.mjs.map +1 -0
  29. package/lib/{chunk-EFB5OFM7.mjs → chunk-P3NFCKTZ.mjs} +6 -4
  30. package/lib/{chunk-EFB5OFM7.mjs.map → chunk-P3NFCKTZ.mjs.map} +1 -1
  31. package/lib/{chunk-M7Y3BOQW.mjs → chunk-Q3MKITPY.mjs} +5 -5
  32. package/lib/chunk-Q64MOYJ7.mjs +218 -0
  33. package/lib/chunk-Q64MOYJ7.mjs.map +1 -0
  34. package/lib/chunk-RQKJNMX5.mjs +89 -0
  35. package/lib/chunk-RQKJNMX5.mjs.map +1 -0
  36. package/lib/{chunk-ZWSGM6PZ.mjs → chunk-SD7J3N3C.mjs} +2 -2
  37. package/lib/{chunk-7RZHFI77.mjs → chunk-VESULYQQ.mjs} +2 -2
  38. package/lib/{chunk-AOSEKL7U.mjs → chunk-WOTU36P3.mjs} +6 -103
  39. package/lib/chunk-WOTU36P3.mjs.map +1 -0
  40. package/lib/{chunk-X5E4YJGZ.mjs → chunk-YPTJJ35S.mjs} +2 -2
  41. package/lib/counter-apply-operation-DZM3MIDm.d.mts +63 -0
  42. package/lib/counter-apply-operation-DZM3MIDm.d.ts +63 -0
  43. package/lib/counter-maintenance.handler.d.mts +38 -0
  44. package/lib/counter-maintenance.handler.d.ts +38 -0
  45. package/lib/counter-maintenance.handler.js +2885 -0
  46. package/lib/counter-maintenance.handler.js.map +1 -0
  47. package/lib/counter-maintenance.handler.mjs +180 -0
  48. package/lib/counter-maintenance.handler.mjs.map +1 -0
  49. package/lib/counter-reconciliation.handler.d.mts +116 -0
  50. package/lib/counter-reconciliation.handler.d.ts +116 -0
  51. package/lib/counter-reconciliation.handler.js +3324 -0
  52. package/lib/counter-reconciliation.handler.js.map +1 -0
  53. package/lib/counter-reconciliation.handler.mjs +295 -0
  54. package/lib/counter-reconciliation.handler.mjs.map +1 -0
  55. package/lib/data-store-postgres-replication.handler.js +50 -2
  56. package/lib/data-store-postgres-replication.handler.js.map +1 -1
  57. package/lib/data-store-postgres-replication.handler.mjs +2 -2
  58. package/lib/delete-chunk.handler.js +118 -2
  59. package/lib/delete-chunk.handler.js.map +1 -1
  60. package/lib/delete-chunk.handler.mjs +3 -3
  61. package/lib/finalize.handler.js +50 -2
  62. package/lib/finalize.handler.js.map +1 -1
  63. package/lib/finalize.handler.mjs +4 -4
  64. package/lib/firehose-archive-transform.handler.js +50 -2
  65. package/lib/firehose-archive-transform.handler.js.map +1 -1
  66. package/lib/firehose-archive-transform.handler.mjs +2 -2
  67. package/lib/index.d.mts +140 -2
  68. package/lib/index.d.ts +143 -5
  69. package/lib/index.js +493 -196
  70. package/lib/index.js.map +1 -1
  71. package/lib/index.mjs +360 -193
  72. package/lib/index.mjs.map +1 -1
  73. package/lib/list-chunks.handler.js +118 -2
  74. package/lib/list-chunks.handler.js.map +1 -1
  75. package/lib/list-chunks.handler.mjs +3 -3
  76. package/lib/platform-deploy-bridge.handler.js +50 -2
  77. package/lib/platform-deploy-bridge.handler.js.map +1 -1
  78. package/lib/platform-deploy-bridge.handler.mjs +1 -1
  79. package/lib/pre-token-generation.handler.js +68 -0
  80. package/lib/pre-token-generation.handler.js.map +1 -1
  81. package/lib/pre-token-generation.handler.mjs +9 -5
  82. package/lib/pre-token-generation.handler.mjs.map +1 -1
  83. package/lib/provision-default-workspace.handler.js +883 -0
  84. package/lib/provision-default-workspace.handler.js.map +1 -1
  85. package/lib/provision-default-workspace.handler.mjs +10 -5
  86. package/lib/provision-default-workspace.handler.mjs.map +1 -1
  87. package/lib/rename-finalize.handler.js +50 -2
  88. package/lib/rename-finalize.handler.js.map +1 -1
  89. package/lib/rename-finalize.handler.mjs +2 -2
  90. package/lib/rename-list-targets.handler.js +118 -2
  91. package/lib/rename-list-targets.handler.js.map +1 -1
  92. package/lib/rename-list-targets.handler.mjs +11 -9
  93. package/lib/rename-list-targets.handler.mjs.map +1 -1
  94. package/lib/rename-rewrite-chunk.handler.js +68 -0
  95. package/lib/rename-rewrite-chunk.handler.js.map +1 -1
  96. package/lib/rename-rewrite-chunk.handler.mjs +2 -2
  97. package/lib/rest-api-lambda.handler.js +1454 -251
  98. package/lib/rest-api-lambda.handler.js.map +1 -1
  99. package/lib/rest-api-lambda.handler.mjs +415 -291
  100. package/lib/rest-api-lambda.handler.mjs.map +1 -1
  101. package/lib/seed-demo-data.handler.js +205 -8
  102. package/lib/seed-demo-data.handler.js.map +1 -1
  103. package/lib/seed-demo-data.handler.mjs +10 -7
  104. package/lib/seed-system-data.handler.js +118 -2
  105. package/lib/seed-system-data.handler.js.map +1 -1
  106. package/lib/seed-system-data.handler.mjs +5 -5
  107. package/package.json +1 -1
  108. package/lib/chunk-7BQHLC7U.mjs.map +0 -1
  109. package/lib/chunk-AOSEKL7U.mjs.map +0 -1
  110. package/lib/chunk-CFJDATDK.mjs.map +0 -1
  111. package/lib/chunk-HQ67J7BP.mjs.map +0 -1
  112. package/lib/chunk-MVQWAIMC.mjs.map +0 -1
  113. package/lib/chunk-ZM4GDHHC.mjs.map +0 -1
  114. /package/lib/{chunk-WPCBVDFZ.mjs.map → chunk-76UM2LQ5.mjs.map} +0 -0
  115. /package/lib/{chunk-QFHYTCVY.mjs.map → chunk-7TRO2STL.mjs.map} +0 -0
  116. /package/lib/{chunk-23PUSHBV.mjs.map → chunk-D2Y6DDOC.mjs.map} +0 -0
  117. /package/lib/{chunk-KR2Y2CVQ.mjs.map → chunk-KA3OMP3X.mjs.map} +0 -0
  118. /package/lib/{chunk-M7Y3BOQW.mjs.map → chunk-Q3MKITPY.mjs.map} +0 -0
  119. /package/lib/{chunk-ZWSGM6PZ.mjs.map → chunk-SD7J3N3C.mjs.map} +0 -0
  120. /package/lib/{chunk-7RZHFI77.mjs.map → chunk-VESULYQQ.mjs.map} +0 -0
  121. /package/lib/{chunk-X5E4YJGZ.mjs.map → chunk-YPTJJ35S.mjs.map} +0 -0
@@ -3,7 +3,14 @@ import {
3
3
  } from "./chunk-2O3CXY2C.mjs";
4
4
  import {
5
5
  createRoleOperation
6
- } from "./chunk-ZWSGM6PZ.mjs";
6
+ } from "./chunk-SD7J3N3C.mjs";
7
+ import {
8
+ countMembershipsByUserOperation,
9
+ listTenantsOperation,
10
+ listWorkspacesOperation,
11
+ membershipListByWorkspaceOperation,
12
+ roleAssignmentListByWorkspaceOperation
13
+ } from "./chunk-Q64MOYJ7.mjs";
7
14
  import {
8
15
  createAccountOperation,
9
16
  createAppointmentOperation,
@@ -19,27 +26,37 @@ import {
19
26
  createPractitionerOperation,
20
27
  createProcedureOperation,
21
28
  getRoleByIdOperation
22
- } from "./chunk-EFB5OFM7.mjs";
29
+ } from "./chunk-P3NFCKTZ.mjs";
23
30
  import {
24
- listMembershipsOperation,
25
31
  listPractitionerRolesOperation,
26
32
  listRoleAssignmentsOperation
27
- } from "./chunk-7BQHLC7U.mjs";
33
+ } from "./chunk-P3CTZWC2.mjs";
34
+ import {
35
+ listMembershipsOperation
36
+ } from "./chunk-GJTPXJKD.mjs";
28
37
  import {
29
38
  createMembershipOperation,
30
39
  createRoleAssignmentOperation,
31
40
  createTenantOperation,
32
41
  createWorkspaceOperation,
33
42
  extractDenormalizedReferenceDisplay
34
- } from "./chunk-CFJDATDK.mjs";
43
+ } from "./chunk-MLFMW5IF.mjs";
44
+ import {
45
+ extractRoleLevel,
46
+ publishMembershipDeleted,
47
+ publishRoleAssignmentDeleted,
48
+ publishWorkspaceDeleted
49
+ } from "./chunk-BUAYVN3C.mjs";
35
50
  import {
36
- buildMembershipUserProjectionItem,
37
51
  buildMembershipWorkspaceProjectionItem,
38
52
  buildRoleAssignmentUserProjectionItem,
39
53
  buildRoleAssignmentWorkspaceProjectionItem,
40
- extractReferenceSlug,
41
- extractReferenceSlug2
42
- } from "./chunk-HQ67J7BP.mjs";
54
+ extractReferenceSlug as extractReferenceSlug2
55
+ } from "./chunk-5S6VFBLT.mjs";
56
+ import {
57
+ buildMembershipUserProjectionItem,
58
+ extractReferenceSlug
59
+ } from "./chunk-I6LUPJUY.mjs";
43
60
  import {
44
61
  executeMultiWrite
45
62
  } from "./chunk-QJDHVMKT.mjs";
@@ -48,11 +65,16 @@ import {
48
65
  deleteUserOperation,
49
66
  findUserBySubOperation,
50
67
  getUserByIdOperation,
51
- listUsersOperation,
52
- membershipListByUserOperation,
53
68
  switchUserTenantWorkspaceOperation,
54
69
  updateUserOperation
55
- } from "./chunk-AOSEKL7U.mjs";
70
+ } from "./chunk-WOTU36P3.mjs";
71
+ import {
72
+ listUsersOperation,
73
+ membershipListByUserOperation
74
+ } from "./chunk-DWSWCUZR.mjs";
75
+ import {
76
+ getDynamoDataService
77
+ } from "./chunk-6BB4CRSS.mjs";
56
78
  import {
57
79
  batchGetWithRetry,
58
80
  buildUpdatedResourceWithAudit,
@@ -62,11 +84,10 @@ import {
62
84
  deleteDataEntityById,
63
85
  dispatchListMode,
64
86
  getDataEntityById,
65
- getDynamoDataService,
66
87
  listDataEntitiesByWorkspace,
67
88
  mergeAuditIntoMeta,
68
89
  updateDataEntityById
69
- } from "./chunk-MVQWAIMC.mjs";
90
+ } from "./chunk-O5VQWB6U.mjs";
70
91
  import {
71
92
  ConflictError,
72
93
  ForbiddenError,
@@ -77,8 +98,9 @@ import {
77
98
  import {
78
99
  SHARD_COUNT,
79
100
  getDynamoControlService
80
- } from "./chunk-VZCPGQXA.mjs";
101
+ } from "./chunk-EUIP2U5F.mjs";
81
102
  import "./chunk-TRY7JGWO.mjs";
103
+ import "./chunk-KMEWULMX.mjs";
82
104
  import "./chunk-LZOMFHX3.mjs";
83
105
 
84
106
  // src/data/lambda/rest-api-lambda.handler.ts
@@ -1052,7 +1074,7 @@ function sendInvalidSummary400(res, diagnostics) {
1052
1074
  }
1053
1075
  async function handleListRoute(opts) {
1054
1076
  const { req, res, basePath, listOperation, errorLogContext } = opts;
1055
- const ctx = req.openhiContext;
1077
+ const ctx = opts.context ?? req.openhiContext;
1056
1078
  const parsed = parseSearchSubsetting(req, res);
1057
1079
  if ("errorResponse" in parsed) return parsed.errorResponse;
1058
1080
  try {
@@ -1732,6 +1754,14 @@ async function deleteMembershipOperation(params) {
1732
1754
  });
1733
1755
  }
1734
1756
  await executeMultiWrite({ service, triples });
1757
+ await publishMembershipDeleted(context, {
1758
+ membershipId: id,
1759
+ tenantId: context.tenantId,
1760
+ ...userIdFromResource !== void 0 && { userId: userIdFromResource },
1761
+ ...workspaceIdFromResource !== void 0 && {
1762
+ workspaceId: workspaceIdFromResource
1763
+ }
1764
+ });
1735
1765
  }
1736
1766
 
1737
1767
  // src/data/rest-api/routes/control/membership/membership-delete-route.ts
@@ -1783,14 +1813,154 @@ async function getMembershipByIdRoute(req, res) {
1783
1813
  }
1784
1814
  }
1785
1815
 
1816
+ // src/data/rest-api/routes/control/cross-cutting-route-helpers.ts
1817
+ function sendInvalidQuery400(res, diagnostics) {
1818
+ return res.status(400).json({
1819
+ resourceType: "OperationOutcome",
1820
+ issue: [{ severity: "error", code: "invalid", diagnostics }]
1821
+ });
1822
+ }
1823
+ function resolveTenantScopeOverride(req, res, context) {
1824
+ const raw = (req.query ?? {}).tenant;
1825
+ if (raw === void 0) {
1826
+ return { ok: true, context };
1827
+ }
1828
+ if (typeof raw !== "string" || raw.length === 0) {
1829
+ return {
1830
+ ok: false,
1831
+ response: sendInvalidQuery400(
1832
+ res,
1833
+ "Query parameter `tenant` must be a non-empty string."
1834
+ )
1835
+ };
1836
+ }
1837
+ return { ok: true, context: { ...context, tenantId: raw } };
1838
+ }
1839
+ function sendForbidden403(res, diagnostics) {
1840
+ return res.status(403).json({
1841
+ resourceType: "OperationOutcome",
1842
+ issue: [{ severity: "error", code: "forbidden", diagnostics }]
1843
+ });
1844
+ }
1845
+ var COMMON_KEYS = ["cursor", "limit", "order"];
1846
+ function parseCommonListQuery(req, res, options = {}) {
1847
+ const q = req.query ?? {};
1848
+ const allowedKeys = /* @__PURE__ */ new Set([
1849
+ ...COMMON_KEYS,
1850
+ ...options.extraKeys ?? []
1851
+ ]);
1852
+ const extra = Object.keys(q).filter((k) => !allowedKeys.has(k));
1853
+ if (extra.length > 0) {
1854
+ return {
1855
+ ok: false,
1856
+ response: sendInvalidQuery400(
1857
+ res,
1858
+ `Unsupported query parameter${extra.length === 1 ? "" : "s"}: ${extra.join(", ")}.`
1859
+ )
1860
+ };
1861
+ }
1862
+ const rawCursor = q.cursor;
1863
+ let cursor = null;
1864
+ if (rawCursor !== void 0) {
1865
+ if (typeof rawCursor !== "string") {
1866
+ return {
1867
+ ok: false,
1868
+ response: sendInvalidQuery400(
1869
+ res,
1870
+ "Query parameter `cursor` must be a string."
1871
+ )
1872
+ };
1873
+ }
1874
+ cursor = rawCursor;
1875
+ }
1876
+ const rawLimit = q.limit;
1877
+ let limit;
1878
+ if (rawLimit !== void 0) {
1879
+ if (typeof rawLimit !== "string" || rawLimit.length === 0) {
1880
+ return {
1881
+ ok: false,
1882
+ response: sendInvalidQuery400(
1883
+ res,
1884
+ "Query parameter `limit` must be a positive integer."
1885
+ )
1886
+ };
1887
+ }
1888
+ const parsed = Number(rawLimit);
1889
+ if (!Number.isInteger(parsed) || parsed <= 0) {
1890
+ return {
1891
+ ok: false,
1892
+ response: sendInvalidQuery400(
1893
+ res,
1894
+ "Query parameter `limit` must be a positive integer."
1895
+ )
1896
+ };
1897
+ }
1898
+ limit = parsed;
1899
+ }
1900
+ const rawOrder = q.order;
1901
+ let order;
1902
+ if (rawOrder !== void 0) {
1903
+ if (rawOrder !== "asc" && rawOrder !== "desc") {
1904
+ return {
1905
+ ok: false,
1906
+ response: sendInvalidQuery400(
1907
+ res,
1908
+ 'Query parameter `order` must be one of "asc", "desc".'
1909
+ )
1910
+ };
1911
+ }
1912
+ order = rawOrder;
1913
+ }
1914
+ const value = order === void 0 ? limit === void 0 ? { cursor } : { cursor, limit } : limit === void 0 ? { cursor, order } : { cursor, limit, order };
1915
+ return { ok: true, value };
1916
+ }
1917
+ function buildPaginationLinks(opts) {
1918
+ const links = [
1919
+ { relation: "self", url: composeUrl(opts.basePath, opts.query) }
1920
+ ];
1921
+ if (opts.nextCursor !== null && opts.nextCursor.length > 0) {
1922
+ const nextQuery = { ...opts.query };
1923
+ nextQuery.cursor = opts.nextCursor;
1924
+ links.push({
1925
+ relation: "next",
1926
+ url: composeUrl(opts.basePath, nextQuery)
1927
+ });
1928
+ }
1929
+ return links;
1930
+ }
1931
+ function composeUrl(basePath, query) {
1932
+ const usp = new URLSearchParams();
1933
+ for (const [key, value] of Object.entries(query)) {
1934
+ if (value === void 0 || value === null) {
1935
+ continue;
1936
+ }
1937
+ if (Array.isArray(value)) {
1938
+ for (const v of value) {
1939
+ if (v !== void 0 && v !== null) {
1940
+ usp.append(key, String(v));
1941
+ }
1942
+ }
1943
+ } else {
1944
+ usp.append(key, String(value));
1945
+ }
1946
+ }
1947
+ const qs = usp.toString();
1948
+ return qs.length === 0 ? basePath : `${basePath}?${qs}`;
1949
+ }
1950
+
1786
1951
  // src/data/rest-api/routes/control/membership/membership-list-route.ts
1787
1952
  async function listMembershipsRoute(req, res) {
1953
+ const scope = resolveTenantScopeOverride(req, res, req.openhiContext);
1954
+ if (!scope.ok) {
1955
+ return scope.response;
1956
+ }
1788
1957
  return handleListRoute({
1789
1958
  req,
1790
1959
  res,
1791
1960
  basePath: BASE_PATH.MEMBERSHIP,
1792
1961
  listOperation: listMembershipsOperation,
1793
- errorLogContext: "GET /Membership list error:"
1962
+ errorLogContext: "GET /Membership list error:",
1963
+ context: scope.context
1794
1964
  });
1795
1965
  }
1796
1966
 
@@ -2277,6 +2447,18 @@ async function deleteRoleAssignmentOperation(params) {
2277
2447
  });
2278
2448
  }
2279
2449
  await executeMultiWrite({ service, triples });
2450
+ await publishRoleAssignmentDeleted(context, {
2451
+ roleAssignmentId: id,
2452
+ tenantId: context.tenantId,
2453
+ ...userIdFromResource !== void 0 && { userId: userIdFromResource },
2454
+ ...workspaceIdFromResource !== void 0 && {
2455
+ workspaceId: workspaceIdFromResource
2456
+ },
2457
+ ...roleIdFromResource !== void 0 && { roleId: roleIdFromResource },
2458
+ ...extractRoleLevel(parsed) !== void 0 && {
2459
+ roleLevel: extractRoleLevel(parsed)
2460
+ }
2461
+ });
2280
2462
  }
2281
2463
 
2282
2464
  // src/data/rest-api/routes/control/roleassignment/roleassignment-delete-route.ts
@@ -2631,42 +2813,6 @@ async function getTenantByIdRoute(req, res) {
2631
2813
  }
2632
2814
  }
2633
2815
 
2634
- // src/data/operations/control/tenant/tenant-list-operation.ts
2635
- var SK7 = "CURRENT";
2636
- async function listTenantsOperation(params) {
2637
- const { tableName, mode = "full" } = params;
2638
- const service = getDynamoControlService(tableName);
2639
- const shardResults = await Promise.all(
2640
- Array.from(
2641
- { length: SHARD_COUNT },
2642
- (_, shard) => service.entities.tenant.query.gsi1({ gsi1Shard: String(shard) }).go()
2643
- )
2644
- );
2645
- return dispatchListMode(
2646
- mode,
2647
- shardResults,
2648
- {
2649
- hydrate: (orderedIds) => batchGetWithRetry(
2650
- service.entities.tenant,
2651
- orderedIds.map((id) => ({ tenantId: id, sk: SK7 }))
2652
- ),
2653
- getId: (item) => item.id,
2654
- buildEntry: (id, item) => ({
2655
- id,
2656
- resource: {
2657
- resourceType: "Tenant",
2658
- id,
2659
- ...JSON.parse(item.resource)
2660
- }
2661
- }),
2662
- buildSummaryEntry: (id, parsed) => ({
2663
- id,
2664
- resource: { resourceType: "Tenant", id, ...parsed }
2665
- })
2666
- }
2667
- );
2668
- }
2669
-
2670
2816
  // src/data/rest-api/routes/control/tenant/tenant-list-route.ts
2671
2817
  async function listTenantsRoute(req, res) {
2672
2818
  return handleListRoute({
@@ -2678,6 +2824,179 @@ async function listTenantsRoute(req, res) {
2678
2824
  });
2679
2825
  }
2680
2826
 
2827
+ // src/data/operations/control/tenant/tenant-users-operation.ts
2828
+ var USER_SK = "CURRENT";
2829
+ function extractEmail(user) {
2830
+ const telecom = user.telecom;
2831
+ if (!Array.isArray(telecom)) {
2832
+ return null;
2833
+ }
2834
+ for (const entry of telecom) {
2835
+ if (entry.system === "email" && typeof entry.value === "string" && entry.value.length > 0) {
2836
+ return entry.value;
2837
+ }
2838
+ }
2839
+ return null;
2840
+ }
2841
+ function extractDisplayName(user) {
2842
+ const name = user.name;
2843
+ if (!Array.isArray(name) || name.length === 0) {
2844
+ return null;
2845
+ }
2846
+ const first = name[0];
2847
+ if (typeof first.text === "string" && first.text.trim().length > 0) {
2848
+ return first.text.trim();
2849
+ }
2850
+ const given = Array.isArray(first.given) ? first.given.filter((g) => typeof g === "string").join(" ").trim() : "";
2851
+ const family = typeof first.family === "string" ? first.family : "";
2852
+ const composed = `${given} ${family}`.trim();
2853
+ return composed.length > 0 ? composed : null;
2854
+ }
2855
+ function extractReferenceDisplay(resource, fieldName) {
2856
+ const field = resource[fieldName];
2857
+ if (!field || typeof field !== "object") {
2858
+ return null;
2859
+ }
2860
+ const display = field.display;
2861
+ if (typeof display !== "string") {
2862
+ return null;
2863
+ }
2864
+ const trimmed = display.trim();
2865
+ return trimmed.length > 0 ? trimmed : null;
2866
+ }
2867
+ function ensureAccumulator(byUser, userId) {
2868
+ let acc = byUser.get(userId);
2869
+ if (!acc) {
2870
+ acc = { tenantRole: null, workspaces: /* @__PURE__ */ new Map() };
2871
+ byUser.set(userId, acc);
2872
+ }
2873
+ return acc;
2874
+ }
2875
+ async function tenantUsersOperation(params) {
2876
+ const { context, tableName } = params;
2877
+ const service = getDynamoControlService(tableName);
2878
+ const [memberships, roleAssignments, workspaces] = await Promise.all([
2879
+ listMembershipsOperation({ context, tableName }),
2880
+ listRoleAssignmentsOperation({ context, tableName }),
2881
+ listWorkspacesOperation({ context, tableName })
2882
+ ]);
2883
+ const workspaceNames = /* @__PURE__ */ new Map();
2884
+ for (const entry of workspaces.entries) {
2885
+ const resource = entry.resource;
2886
+ const name = typeof resource.name === "string" && resource.name.trim().length > 0 ? resource.name.trim() : null;
2887
+ workspaceNames.set(entry.id, name);
2888
+ }
2889
+ const byUser = /* @__PURE__ */ new Map();
2890
+ for (const entry of memberships.entries) {
2891
+ const resource = entry.resource;
2892
+ const userId = extractReferenceSlug(resource, "user");
2893
+ if (userId === void 0) {
2894
+ continue;
2895
+ }
2896
+ const acc = ensureAccumulator(byUser, userId);
2897
+ const workspaceId = extractReferenceSlug(resource, "workspace");
2898
+ if (workspaceId !== void 0 && !acc.workspaces.has(workspaceId)) {
2899
+ acc.workspaces.set(workspaceId, { workspaceId, role: null });
2900
+ }
2901
+ }
2902
+ for (const entry of roleAssignments.entries) {
2903
+ const resource = entry.resource;
2904
+ const userId = extractReferenceSlug(resource, "user");
2905
+ const roleId = extractReferenceSlug(resource, "role");
2906
+ if (userId === void 0 || roleId === void 0) {
2907
+ continue;
2908
+ }
2909
+ const acc = ensureAccumulator(byUser, userId);
2910
+ const role = {
2911
+ roleId,
2912
+ roleName: extractReferenceDisplay(resource, "role")
2913
+ };
2914
+ const workspaceId = extractReferenceSlug(resource, "workspace");
2915
+ if (workspaceId === void 0) {
2916
+ acc.tenantRole = role;
2917
+ } else {
2918
+ const existing = acc.workspaces.get(workspaceId);
2919
+ if (existing) {
2920
+ existing.role = role;
2921
+ } else {
2922
+ acc.workspaces.set(workspaceId, { workspaceId, role });
2923
+ }
2924
+ }
2925
+ }
2926
+ const userIds = Array.from(byUser.keys());
2927
+ const userRows = userIds.length === 0 ? [] : await batchGetWithRetry(
2928
+ service.entities.user,
2929
+ userIds.map((id) => ({ id, sk: USER_SK }))
2930
+ );
2931
+ const usersById = /* @__PURE__ */ new Map();
2932
+ for (const row of userRows) {
2933
+ try {
2934
+ usersById.set(
2935
+ row.id,
2936
+ JSON.parse(row.resource)
2937
+ );
2938
+ } catch {
2939
+ }
2940
+ }
2941
+ const entries = userIds.map((userId) => {
2942
+ const acc = byUser.get(userId);
2943
+ const user = usersById.get(userId);
2944
+ const workspaceEntries = Array.from(
2945
+ acc.workspaces.values()
2946
+ ).map((ws) => ({
2947
+ workspaceId: ws.workspaceId,
2948
+ workspaceName: workspaceNames.get(ws.workspaceId) ?? null,
2949
+ role: ws.role
2950
+ }));
2951
+ return {
2952
+ resourceType: "TenantUser",
2953
+ userId,
2954
+ displayName: user ? extractDisplayName(user) : null,
2955
+ email: user ? extractEmail(user) : null,
2956
+ tenantRole: acc.tenantRole,
2957
+ workspaces: workspaceEntries
2958
+ };
2959
+ });
2960
+ return { entries };
2961
+ }
2962
+
2963
+ // src/data/rest-api/routes/control/tenant/tenant-list-users-route.ts
2964
+ async function listTenantUsersRoute(req, res) {
2965
+ const ctx = req.openhiContext;
2966
+ if (!ctx) {
2967
+ return sendForbidden403(
2968
+ res,
2969
+ "Missing or invalid OpenHI JWT claims (tenant, workspace, or audit context)."
2970
+ );
2971
+ }
2972
+ const tenantId = String(req.params.id);
2973
+ if (tenantId.length === 0) {
2974
+ return sendForbidden403(res, "Tenant id is required.");
2975
+ }
2976
+ const parsed = parseCommonListQuery(req, res);
2977
+ if (!parsed.ok) {
2978
+ return parsed.response;
2979
+ }
2980
+ const scopedContext = { ...ctx, tenantId };
2981
+ try {
2982
+ const result = await tenantUsersOperation({ context: scopedContext });
2983
+ const basePath = `${BASE_PATH.TENANT}/${tenantId}/users`;
2984
+ const entries = result.entries.map((entry) => ({
2985
+ fullUrl: `${BASE_PATH.USER}/${entry.userId}`,
2986
+ resource: entry
2987
+ }));
2988
+ return res.json({
2989
+ resourceType: "Bundle",
2990
+ type: "searchset",
2991
+ total: entries.length,
2992
+ link: [{ relation: "self", url: basePath }],
2993
+ entry: entries
2994
+ });
2995
+ } catch (err) {
2996
+ return sendOperationOutcome500(res, err, "GET /Tenant/:id/users error:");
2997
+ }
2998
+ }
2999
+
2681
3000
  // src/data/operations/control/tenant/tenant-update-operation.ts
2682
3001
  import { extractSummary as extractSummary4 } from "@openhi/types";
2683
3002
  async function updateTenantOperation(params) {
@@ -2745,6 +3064,7 @@ async function updateTenantRoute(req, res) {
2745
3064
  // src/data/rest-api/routes/control/tenant/tenant.ts
2746
3065
  var router6 = express6.Router();
2747
3066
  router6.get("/", listTenantsRoute);
3067
+ router6.get("/:id/users", listTenantUsersRoute);
2748
3068
  router6.get("/:id", getTenantByIdRoute);
2749
3069
  router6.post("/", createTenantRoute);
2750
3070
  router6.put("/:id", updateTenantRoute);
@@ -2892,125 +3212,6 @@ async function configurationListByWorkspaceOperation(params) {
2892
3212
  return { items, cursor: result.cursor ?? null };
2893
3213
  }
2894
3214
 
2895
- // src/data/rest-api/routes/control/cross-cutting-route-helpers.ts
2896
- function sendInvalidQuery400(res, diagnostics) {
2897
- return res.status(400).json({
2898
- resourceType: "OperationOutcome",
2899
- issue: [{ severity: "error", code: "invalid", diagnostics }]
2900
- });
2901
- }
2902
- function sendForbidden403(res, diagnostics) {
2903
- return res.status(403).json({
2904
- resourceType: "OperationOutcome",
2905
- issue: [{ severity: "error", code: "forbidden", diagnostics }]
2906
- });
2907
- }
2908
- var COMMON_KEYS = ["cursor", "limit", "order"];
2909
- function parseCommonListQuery(req, res, options = {}) {
2910
- const q = req.query ?? {};
2911
- const allowedKeys = /* @__PURE__ */ new Set([
2912
- ...COMMON_KEYS,
2913
- ...options.extraKeys ?? []
2914
- ]);
2915
- const extra = Object.keys(q).filter((k) => !allowedKeys.has(k));
2916
- if (extra.length > 0) {
2917
- return {
2918
- ok: false,
2919
- response: sendInvalidQuery400(
2920
- res,
2921
- `Unsupported query parameter${extra.length === 1 ? "" : "s"}: ${extra.join(", ")}.`
2922
- )
2923
- };
2924
- }
2925
- const rawCursor = q.cursor;
2926
- let cursor = null;
2927
- if (rawCursor !== void 0) {
2928
- if (typeof rawCursor !== "string") {
2929
- return {
2930
- ok: false,
2931
- response: sendInvalidQuery400(
2932
- res,
2933
- "Query parameter `cursor` must be a string."
2934
- )
2935
- };
2936
- }
2937
- cursor = rawCursor;
2938
- }
2939
- const rawLimit = q.limit;
2940
- let limit;
2941
- if (rawLimit !== void 0) {
2942
- if (typeof rawLimit !== "string" || rawLimit.length === 0) {
2943
- return {
2944
- ok: false,
2945
- response: sendInvalidQuery400(
2946
- res,
2947
- "Query parameter `limit` must be a positive integer."
2948
- )
2949
- };
2950
- }
2951
- const parsed = Number(rawLimit);
2952
- if (!Number.isInteger(parsed) || parsed <= 0) {
2953
- return {
2954
- ok: false,
2955
- response: sendInvalidQuery400(
2956
- res,
2957
- "Query parameter `limit` must be a positive integer."
2958
- )
2959
- };
2960
- }
2961
- limit = parsed;
2962
- }
2963
- const rawOrder = q.order;
2964
- let order;
2965
- if (rawOrder !== void 0) {
2966
- if (rawOrder !== "asc" && rawOrder !== "desc") {
2967
- return {
2968
- ok: false,
2969
- response: sendInvalidQuery400(
2970
- res,
2971
- 'Query parameter `order` must be one of "asc", "desc".'
2972
- )
2973
- };
2974
- }
2975
- order = rawOrder;
2976
- }
2977
- const value = order === void 0 ? limit === void 0 ? { cursor } : { cursor, limit } : limit === void 0 ? { cursor, order } : { cursor, limit, order };
2978
- return { ok: true, value };
2979
- }
2980
- function buildPaginationLinks(opts) {
2981
- const links = [
2982
- { relation: "self", url: composeUrl(opts.basePath, opts.query) }
2983
- ];
2984
- if (opts.nextCursor !== null && opts.nextCursor.length > 0) {
2985
- const nextQuery = { ...opts.query };
2986
- nextQuery.cursor = opts.nextCursor;
2987
- links.push({
2988
- relation: "next",
2989
- url: composeUrl(opts.basePath, nextQuery)
2990
- });
2991
- }
2992
- return links;
2993
- }
2994
- function composeUrl(basePath, query) {
2995
- const usp = new URLSearchParams();
2996
- for (const [key, value] of Object.entries(query)) {
2997
- if (value === void 0 || value === null) {
2998
- continue;
2999
- }
3000
- if (Array.isArray(value)) {
3001
- for (const v of value) {
3002
- if (v !== void 0 && v !== null) {
3003
- usp.append(key, String(v));
3004
- }
3005
- }
3006
- } else {
3007
- usp.append(key, String(value));
3008
- }
3009
- }
3010
- const qs = usp.toString();
3011
- return qs.length === 0 ? basePath : `${basePath}?${qs}`;
3012
- }
3013
-
3014
3215
  // src/data/rest-api/routes/control/projection-bundle-helpers.ts
3015
3216
  var EXT_BASE = "https://openhi.org/fhir/StructureDefinition";
3016
3217
  function parseProjectionSummary(summary) {
@@ -3267,41 +3468,6 @@ async function listUserConfigurationsRoute(req, res) {
3267
3468
  }
3268
3469
  }
3269
3470
 
3270
- // src/data/operations/control/membership/membership-list-by-workspace-operation.ts
3271
- async function membershipListByWorkspaceOperation(params) {
3272
- const {
3273
- tenantId,
3274
- workspaceId,
3275
- cursor = null,
3276
- limit,
3277
- order,
3278
- tableName
3279
- } = params;
3280
- const service = getDynamoControlService(tableName);
3281
- const goOptions = {
3282
- cursor
3283
- };
3284
- if (limit !== void 0) {
3285
- goOptions.limit = limit;
3286
- }
3287
- if (order !== void 0) {
3288
- goOptions.order = order;
3289
- }
3290
- const result = await service.entities.membershipWorkspaceProjection.query.record({ tenantId, workspaceId }).begins({ sk: "MEMBERSHIP#" }).go(goOptions);
3291
- const items = (result.data ?? []).map((row) => ({
3292
- tenantId: row.tenantId,
3293
- workspaceId: row.workspaceId,
3294
- sk: row.sk,
3295
- userId: row.userId,
3296
- membershipId: row.membershipId,
3297
- summary: row.summary,
3298
- vid: row.vid,
3299
- lastUpdated: row.lastUpdated,
3300
- denormalizedUserName: row.denormalizedUserName
3301
- }));
3302
- return { items, cursor: result.cursor ?? null };
3303
- }
3304
-
3305
3471
  // src/data/rest-api/routes/control/user/user-list-memberships-route.ts
3306
3472
  var ALLOWED_MODES = [
3307
3473
  "all",
@@ -3325,7 +3491,7 @@ async function listUserMembershipsRoute(req, res) {
3325
3491
  );
3326
3492
  }
3327
3493
  const parsed = parseCommonListQuery(req, res, {
3328
- extraKeys: ["mode", "tenantId"]
3494
+ extraKeys: ["mode", "tenantId", "_summary"]
3329
3495
  });
3330
3496
  if (!parsed.ok) {
3331
3497
  return parsed.response;
@@ -3358,6 +3524,33 @@ async function listUserMembershipsRoute(req, res) {
3358
3524
  'Query parameter `tenantId` is required when `mode === "workspaceInTenant"`.'
3359
3525
  );
3360
3526
  }
3527
+ const rawSummary = req.query._summary;
3528
+ if (rawSummary !== void 0) {
3529
+ if (rawSummary !== "count") {
3530
+ return sendInvalidQuery400(
3531
+ res,
3532
+ 'Query parameter `_summary` must be "count" on this endpoint.'
3533
+ );
3534
+ }
3535
+ try {
3536
+ const total = await countMembershipsByUserOperation({
3537
+ userId,
3538
+ mode,
3539
+ tenantId
3540
+ });
3541
+ return res.json({
3542
+ resourceType: "Bundle",
3543
+ type: "searchset",
3544
+ total
3545
+ });
3546
+ } catch (err) {
3547
+ return sendOperationOutcome500(
3548
+ res,
3549
+ err,
3550
+ "GET /User/:id/Membership count error:"
3551
+ );
3552
+ }
3553
+ }
3361
3554
  try {
3362
3555
  const result = await membershipListByUserOperation({
3363
3556
  userId,
@@ -3454,51 +3647,6 @@ async function roleAssignmentListByUserOperation(params) {
3454
3647
  return { items, cursor: result.cursor ?? null };
3455
3648
  }
3456
3649
 
3457
- // src/data/operations/control/roleassignment/roleassignment-list-by-workspace-operation.ts
3458
- function buildSkPrefix2(roleId) {
3459
- if (roleId === void 0 || roleId.length === 0) {
3460
- return "ROLEASSIGNMENT#";
3461
- }
3462
- return `ROLEASSIGNMENT#${roleId}#`;
3463
- }
3464
- async function roleAssignmentListByWorkspaceOperation(params) {
3465
- const {
3466
- tenantId,
3467
- workspaceId,
3468
- roleId,
3469
- cursor = null,
3470
- limit,
3471
- order,
3472
- tableName
3473
- } = params;
3474
- const service = getDynamoControlService(tableName);
3475
- const skPrefix = buildSkPrefix2(roleId);
3476
- const goOptions = {
3477
- cursor
3478
- };
3479
- if (limit !== void 0) {
3480
- goOptions.limit = limit;
3481
- }
3482
- if (order !== void 0) {
3483
- goOptions.order = order;
3484
- }
3485
- const result = await service.entities.roleAssignmentWorkspaceProjection.query.record({ tenantId, workspaceId }).begins({ sk: skPrefix }).go(goOptions);
3486
- const items = (result.data ?? []).map((row) => ({
3487
- tenantId: row.tenantId,
3488
- workspaceId: row.workspaceId,
3489
- sk: row.sk,
3490
- userId: row.userId,
3491
- roleId: row.roleId,
3492
- roleAssignmentId: row.roleAssignmentId,
3493
- summary: row.summary,
3494
- vid: row.vid,
3495
- lastUpdated: row.lastUpdated,
3496
- denormalizedUserName: row.denormalizedUserName,
3497
- denormalizedRoleName: row.denormalizedRoleName
3498
- }));
3499
- return { items, cursor: result.cursor ?? null };
3500
- }
3501
-
3502
3650
  // src/data/rest-api/routes/control/user/user-list-role-assignments-route.ts
3503
3651
  var ALLOWED_MODES2 = [
3504
3652
  "all",
@@ -4111,12 +4259,20 @@ async function deleteWorkspaceOperation(params) {
4111
4259
  const { context, id, tableName } = params;
4112
4260
  const { tenantId } = context;
4113
4261
  const service = getDynamoControlService(tableName);
4262
+ const existing = await service.entities.workspace.get({ tenantId, id, sk: "CURRENT" }).go();
4263
+ const workspaceExisted = existing.data !== null;
4114
4264
  await service.entities.workspace.delete({ tenantId, id, sk: "CURRENT" }).go();
4115
4265
  await deleteOrganizationOperation({
4116
4266
  context: { ...context, workspaceId: id },
4117
4267
  id,
4118
4268
  tableName
4119
4269
  });
4270
+ if (workspaceExisted) {
4271
+ await publishWorkspaceDeleted(context, {
4272
+ workspaceId: id,
4273
+ tenantId
4274
+ });
4275
+ }
4120
4276
  }
4121
4277
 
4122
4278
  // src/data/rest-api/routes/control/workspace/workspace-delete-route.ts
@@ -4382,51 +4538,19 @@ async function listWorkspaceRoleAssignmentsRoute(req, res) {
4382
4538
  }
4383
4539
  }
4384
4540
 
4385
- // src/data/operations/control/workspace/workspace-list-operation.ts
4386
- var SK8 = "CURRENT";
4387
- async function listWorkspacesOperation(params) {
4388
- const { context, tableName, mode = "full" } = params;
4389
- const { tenantId } = context;
4390
- const service = getDynamoControlService(tableName);
4391
- const shardResults = await Promise.all(
4392
- Array.from(
4393
- { length: SHARD_COUNT },
4394
- (_, shard) => service.entities.workspace.query.gsi1({ tenantId, gsi1Shard: String(shard) }).go()
4395
- )
4396
- );
4397
- return dispatchListMode(
4398
- mode,
4399
- shardResults,
4400
- {
4401
- hydrate: (orderedIds) => batchGetWithRetry(
4402
- service.entities.workspace,
4403
- orderedIds.map((id) => ({ tenantId, id, sk: SK8 }))
4404
- ),
4405
- getId: (item) => item.id,
4406
- buildEntry: (id, item) => ({
4407
- id,
4408
- resource: {
4409
- resourceType: "Workspace",
4410
- id,
4411
- ...JSON.parse(item.resource)
4412
- }
4413
- }),
4414
- buildSummaryEntry: (id, parsed) => ({
4415
- id,
4416
- resource: { resourceType: "Workspace", id, ...parsed }
4417
- })
4418
- }
4419
- );
4420
- }
4421
-
4422
4541
  // src/data/rest-api/routes/control/workspace/workspace-list-route.ts
4423
4542
  async function listWorkspacesRoute(req, res) {
4543
+ const scope = resolveTenantScopeOverride(req, res, req.openhiContext);
4544
+ if (!scope.ok) {
4545
+ return scope.response;
4546
+ }
4424
4547
  return handleListRoute({
4425
4548
  req,
4426
4549
  res,
4427
4550
  basePath: BASE_PATH.WORKSPACE,
4428
4551
  listOperation: listWorkspacesOperation,
4429
- errorLogContext: "GET /Workspace list error:"
4552
+ errorLogContext: "GET /Workspace list error:",
4553
+ context: scope.context
4430
4554
  });
4431
4555
  }
4432
4556