@openhi/constructs 0.0.111 → 0.0.112

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 (111) hide show
  1. package/lib/chunk-23PUSHBV.mjs +24 -0
  2. package/lib/chunk-23PUSHBV.mjs.map +1 -0
  3. package/lib/{chunk-7FUAMZOF.mjs → chunk-53OHXLIL.mjs} +3 -3
  4. package/lib/chunk-6NBGYGFL.mjs +1803 -0
  5. package/lib/chunk-6NBGYGFL.mjs.map +1 -0
  6. package/lib/chunk-7RZHFI77.mjs +22 -0
  7. package/lib/chunk-7RZHFI77.mjs.map +1 -0
  8. package/lib/{chunk-7Q2IJ2J5.mjs → chunk-CUUKXDB2.mjs} +6 -6
  9. package/lib/chunk-FYHBHHWK.mjs +47 -0
  10. package/lib/chunk-FYHBHHWK.mjs.map +1 -0
  11. package/lib/{chunk-MULKGFIJ.mjs → chunk-GBDIGTNV.mjs} +165 -10
  12. package/lib/chunk-GBDIGTNV.mjs.map +1 -0
  13. package/lib/chunk-HQ67J7BP.mjs +199 -0
  14. package/lib/chunk-HQ67J7BP.mjs.map +1 -0
  15. package/lib/{chunk-AJ3G3THO.mjs → chunk-KO64HPWQ.mjs} +2 -2
  16. package/lib/{chunk-BB5MK4L3.mjs → chunk-KSFC72TT.mjs} +3 -3
  17. package/lib/{chunk-2TPJ6HOF.mjs → chunk-NZRW7ROK.mjs} +72 -54
  18. package/lib/chunk-NZRW7ROK.mjs.map +1 -0
  19. package/lib/chunk-QJDHVMKT.mjs +117 -0
  20. package/lib/chunk-QJDHVMKT.mjs.map +1 -0
  21. package/lib/{chunk-IS4VQRI4.mjs → chunk-QMBJ4VHC.mjs} +12 -47
  22. package/lib/chunk-QMBJ4VHC.mjs.map +1 -0
  23. package/lib/chunk-TRY7JGWO.mjs +16 -0
  24. package/lib/chunk-TRY7JGWO.mjs.map +1 -0
  25. package/lib/chunk-W4KR4CSL.mjs +236 -0
  26. package/lib/chunk-W4KR4CSL.mjs.map +1 -0
  27. package/lib/{chunk-AGF3RAAZ.mjs → chunk-WPCBVDFZ.mjs} +2 -2
  28. package/lib/chunk-WQWFVEVX.mjs +66 -0
  29. package/lib/chunk-WQWFVEVX.mjs.map +1 -0
  30. package/lib/{chunk-SYBADQXI.mjs → chunk-ZM4GDHHC.mjs} +77 -2
  31. package/lib/chunk-ZM4GDHHC.mjs.map +1 -0
  32. package/lib/delete-chunk.handler.d.mts +29 -0
  33. package/lib/delete-chunk.handler.d.ts +29 -0
  34. package/lib/delete-chunk.handler.js +2716 -0
  35. package/lib/delete-chunk.handler.js.map +1 -0
  36. package/lib/delete-chunk.handler.mjs +47 -0
  37. package/lib/delete-chunk.handler.mjs.map +1 -0
  38. package/lib/events-CjS-sm0W.d.mts +107 -0
  39. package/lib/events-CjS-sm0W.d.ts +107 -0
  40. package/lib/events-Da_cFgtc.d.mts +208 -0
  41. package/lib/events-Da_cFgtc.d.ts +208 -0
  42. package/lib/finalize.handler.d.mts +35 -0
  43. package/lib/finalize.handler.d.ts +35 -0
  44. package/lib/finalize.handler.js +875 -0
  45. package/lib/finalize.handler.js.map +1 -0
  46. package/lib/finalize.handler.mjs +166 -0
  47. package/lib/finalize.handler.mjs.map +1 -0
  48. package/lib/index.d.mts +189 -2
  49. package/lib/index.d.ts +500 -3
  50. package/lib/index.js +1753 -174
  51. package/lib/index.js.map +1 -1
  52. package/lib/index.mjs +571 -17
  53. package/lib/index.mjs.map +1 -1
  54. package/lib/list-chunks.handler.d.mts +28 -0
  55. package/lib/list-chunks.handler.d.ts +28 -0
  56. package/lib/list-chunks.handler.js +2746 -0
  57. package/lib/list-chunks.handler.js.map +1 -0
  58. package/lib/list-chunks.handler.mjs +54 -0
  59. package/lib/list-chunks.handler.mjs.map +1 -0
  60. package/lib/platform-deploy-bridge.handler.js +76 -1
  61. package/lib/platform-deploy-bridge.handler.js.map +1 -1
  62. package/lib/platform-deploy-bridge.handler.mjs +1 -1
  63. package/lib/pre-token-generation.handler.js +1106 -155
  64. package/lib/pre-token-generation.handler.js.map +1 -1
  65. package/lib/pre-token-generation.handler.mjs +6 -4
  66. package/lib/pre-token-generation.handler.mjs.map +1 -1
  67. package/lib/provision-default-workspace.handler.js +1529 -142
  68. package/lib/provision-default-workspace.handler.js.map +1 -1
  69. package/lib/provision-default-workspace.handler.mjs +8 -4
  70. package/lib/provision-default-workspace.handler.mjs.map +1 -1
  71. package/lib/rename-finalize.handler.d.mts +30 -0
  72. package/lib/rename-finalize.handler.d.ts +30 -0
  73. package/lib/rename-finalize.handler.js +795 -0
  74. package/lib/rename-finalize.handler.js.map +1 -0
  75. package/lib/rename-finalize.handler.mjs +90 -0
  76. package/lib/rename-finalize.handler.mjs.map +1 -0
  77. package/lib/rename-list-targets.handler.d.mts +26 -0
  78. package/lib/rename-list-targets.handler.d.ts +26 -0
  79. package/lib/rename-list-targets.handler.js +2985 -0
  80. package/lib/rename-list-targets.handler.js.map +1 -0
  81. package/lib/rename-list-targets.handler.mjs +431 -0
  82. package/lib/rename-list-targets.handler.mjs.map +1 -0
  83. package/lib/rename-rewrite-chunk.handler.d.mts +35 -0
  84. package/lib/rename-rewrite-chunk.handler.d.ts +35 -0
  85. package/lib/rename-rewrite-chunk.handler.js +2021 -0
  86. package/lib/rename-rewrite-chunk.handler.js.map +1 -0
  87. package/lib/rename-rewrite-chunk.handler.mjs +27 -0
  88. package/lib/rename-rewrite-chunk.handler.mjs.map +1 -0
  89. package/lib/rest-api-lambda.handler.js +4021 -932
  90. package/lib/rest-api-lambda.handler.js.map +1 -1
  91. package/lib/rest-api-lambda.handler.mjs +1786 -80
  92. package/lib/rest-api-lambda.handler.mjs.map +1 -1
  93. package/lib/seed-demo-data.handler.js +1588 -124
  94. package/lib/seed-demo-data.handler.js.map +1 -1
  95. package/lib/seed-demo-data.handler.mjs +10 -6
  96. package/lib/seed-system-data.handler.js +1179 -155
  97. package/lib/seed-system-data.handler.js.map +1 -1
  98. package/lib/seed-system-data.handler.mjs +5 -4
  99. package/lib/seed-system-data.handler.mjs.map +1 -1
  100. package/package.json +3 -3
  101. package/lib/chunk-2TPJ6HOF.mjs.map +0 -1
  102. package/lib/chunk-IS4VQRI4.mjs.map +0 -1
  103. package/lib/chunk-MULKGFIJ.mjs.map +0 -1
  104. package/lib/chunk-QR5JVSCF.mjs +0 -862
  105. package/lib/chunk-QR5JVSCF.mjs.map +0 -1
  106. package/lib/chunk-SYBADQXI.mjs.map +0 -1
  107. /package/lib/{chunk-7FUAMZOF.mjs.map → chunk-53OHXLIL.mjs.map} +0 -0
  108. /package/lib/{chunk-7Q2IJ2J5.mjs.map → chunk-CUUKXDB2.mjs.map} +0 -0
  109. /package/lib/{chunk-AJ3G3THO.mjs.map → chunk-KO64HPWQ.mjs.map} +0 -0
  110. /package/lib/{chunk-BB5MK4L3.mjs.map → chunk-KSFC72TT.mjs.map} +0 -0
  111. /package/lib/{chunk-AGF3RAAZ.mjs.map → chunk-WPCBVDFZ.mjs.map} +0 -0
@@ -1,36 +1,46 @@
1
- import {
2
- createRoleOperation
3
- } from "./chunk-AJ3G3THO.mjs";
4
1
  import {
5
2
  buildSchemaBootstrapStatements
6
3
  } from "./chunk-2O3CXY2C.mjs";
4
+ import {
5
+ createRoleOperation
6
+ } from "./chunk-KO64HPWQ.mjs";
7
7
  import {
8
8
  getRoleByIdOperation
9
- } from "./chunk-7FUAMZOF.mjs";
9
+ } from "./chunk-53OHXLIL.mjs";
10
10
  import {
11
11
  listMembershipsOperation,
12
12
  listPractitionerRolesOperation,
13
13
  listRoleAssignmentsOperation
14
- } from "./chunk-BB5MK4L3.mjs";
14
+ } from "./chunk-KSFC72TT.mjs";
15
15
  import {
16
16
  createMembershipOperation,
17
17
  createRoleAssignmentOperation,
18
18
  createTenantOperation,
19
- createWorkspaceOperation
20
- } from "./chunk-MULKGFIJ.mjs";
19
+ createWorkspaceOperation,
20
+ extractDenormalizedReferenceDisplay
21
+ } from "./chunk-GBDIGTNV.mjs";
22
+ import {
23
+ buildMembershipUserProjectionItem,
24
+ buildMembershipWorkspaceProjectionItem,
25
+ buildRoleAssignmentUserProjectionItem,
26
+ buildRoleAssignmentWorkspaceProjectionItem,
27
+ extractReferenceSlug,
28
+ extractReferenceSlug2
29
+ } from "./chunk-HQ67J7BP.mjs";
30
+ import {
31
+ executeMultiWrite
32
+ } from "./chunk-QJDHVMKT.mjs";
21
33
  import {
22
34
  createUserOperation,
23
35
  deleteUserOperation,
24
36
  findUserBySubOperation,
25
37
  getUserByIdOperation,
26
38
  listUsersOperation,
39
+ membershipListByUserOperation,
27
40
  switchUserTenantWorkspaceOperation,
28
41
  updateUserOperation
29
- } from "./chunk-2TPJ6HOF.mjs";
42
+ } from "./chunk-NZRW7ROK.mjs";
30
43
  import {
31
- ForbiddenError,
32
- NotFoundError,
33
- ValidationError,
34
44
  batchGetWithRetry,
35
45
  buildUpdatedResourceWithAudit,
36
46
  compressResource,
@@ -38,17 +48,23 @@ import {
38
48
  decompressResource,
39
49
  deleteDataEntityById,
40
50
  dispatchListMode,
41
- domainErrorToHttpStatus,
42
51
  getDataEntityById,
43
52
  getDynamoDataService,
44
53
  listDataEntitiesByWorkspace,
45
54
  mergeAuditIntoMeta,
46
55
  updateDataEntityById
47
- } from "./chunk-IS4VQRI4.mjs";
56
+ } from "./chunk-QMBJ4VHC.mjs";
57
+ import {
58
+ ForbiddenError,
59
+ NotFoundError,
60
+ ValidationError,
61
+ domainErrorToHttpStatus
62
+ } from "./chunk-FYHBHHWK.mjs";
48
63
  import {
49
64
  SHARD_COUNT,
50
65
  getDynamoControlService
51
- } from "./chunk-QR5JVSCF.mjs";
66
+ } from "./chunk-6NBGYGFL.mjs";
67
+ import "./chunk-TRY7JGWO.mjs";
52
68
  import "./chunk-LZOMFHX3.mjs";
53
69
 
54
70
  // src/data/lambda/rest-api-lambda.handler.ts
@@ -119,6 +135,81 @@ function openHiContextMiddleware(req, res, next) {
119
135
  // src/data/rest-api/routes/control/configuration/configuration.ts
120
136
  import express from "express";
121
137
 
138
+ // src/data/operations/control/configuration/configuration-user-projection.ts
139
+ import { normalizeLabel } from "@openhi/types";
140
+ var MISSING_NAME_SENTINEL = "-";
141
+ var ABSENT_USER_SENTINEL = "-";
142
+ function buildConfigurationUserProjectionSk(params) {
143
+ const normalizedConfigName = typeof params.key === "string" && params.key.length > 0 ? normalizeLabel(params.key) : MISSING_NAME_SENTINEL;
144
+ const safeNormalized = normalizedConfigName.length > 0 ? normalizedConfigName : MISSING_NAME_SENTINEL;
145
+ return `CONFIGURATION#${safeNormalized}#${params.configurationId}`;
146
+ }
147
+ function buildConfigurationUserProjectionItem(input) {
148
+ if (!input.userId || input.userId.length === 0 || input.userId === ABSENT_USER_SENTINEL) {
149
+ return void 0;
150
+ }
151
+ if (!input.configurationId || input.configurationId.length === 0) {
152
+ return void 0;
153
+ }
154
+ const sk = buildConfigurationUserProjectionSk({
155
+ key: input.key,
156
+ configurationId: input.configurationId
157
+ });
158
+ return {
159
+ userId: input.userId,
160
+ sk,
161
+ tenantId: input.tenantId,
162
+ configurationId: input.configurationId,
163
+ scope: "user",
164
+ displayName: input.key,
165
+ summary: input.summary,
166
+ vid: input.vid,
167
+ lastUpdated: input.lastUpdated
168
+ };
169
+ }
170
+ function isUserScopedConfiguration(userId) {
171
+ return typeof userId === "string" && userId.length > 0 && userId !== ABSENT_USER_SENTINEL;
172
+ }
173
+
174
+ // src/data/operations/control/configuration/configuration-workspace-projection.ts
175
+ import { normalizeLabel as normalizeLabel2 } from "@openhi/types";
176
+ var MISSING_NAME_SENTINEL2 = "-";
177
+ var ABSENT_WORKSPACE_SENTINEL = "-";
178
+ var ABSENT_USER_SENTINEL2 = "-";
179
+ function buildConfigurationWorkspaceProjectionSk(params) {
180
+ const normalizedConfigName = typeof params.key === "string" && params.key.length > 0 ? normalizeLabel2(params.key) : MISSING_NAME_SENTINEL2;
181
+ const safeNormalized = normalizedConfigName.length > 0 ? normalizedConfigName : MISSING_NAME_SENTINEL2;
182
+ return `CONFIGURATION#${safeNormalized}#${params.configurationId}`;
183
+ }
184
+ function buildConfigurationWorkspaceProjectionItem(input) {
185
+ if (!input.workspaceId || input.workspaceId.length === 0 || input.workspaceId === ABSENT_WORKSPACE_SENTINEL) {
186
+ return void 0;
187
+ }
188
+ if (!input.configurationId || input.configurationId.length === 0) {
189
+ return void 0;
190
+ }
191
+ const sk = buildConfigurationWorkspaceProjectionSk({
192
+ key: input.key,
193
+ configurationId: input.configurationId
194
+ });
195
+ return {
196
+ tenantId: input.tenantId,
197
+ workspaceId: input.workspaceId,
198
+ sk,
199
+ configurationId: input.configurationId,
200
+ scope: "workspace",
201
+ displayName: input.key,
202
+ summary: input.summary,
203
+ vid: input.vid,
204
+ lastUpdated: input.lastUpdated
205
+ };
206
+ }
207
+ function isWorkspaceScopedConfiguration(params) {
208
+ const workspaceIsReal = typeof params.workspaceId === "string" && params.workspaceId.length > 0 && params.workspaceId !== ABSENT_WORKSPACE_SENTINEL;
209
+ const userIsAbsent = typeof params.userId !== "string" || params.userId.length === 0 || params.userId === ABSENT_USER_SENTINEL2;
210
+ return workspaceIsReal && userIsAbsent;
211
+ }
212
+
122
213
  // src/data/operations/control/configuration/configuration-create-operation.ts
123
214
  var SK = "CURRENT";
124
215
  async function createConfigurationOperation(params) {
@@ -147,7 +238,28 @@ async function createConfigurationOperation(params) {
147
238
  roleId
148
239
  });
149
240
  const service = getDynamoControlService(tableName);
150
- await service.entities.configuration.put({
241
+ const userProjectionItem = isUserScopedConfiguration(userId) ? buildConfigurationUserProjectionItem({
242
+ tenantId,
243
+ userId,
244
+ configurationId: id,
245
+ key,
246
+ summary,
247
+ vid,
248
+ lastUpdated
249
+ }) : void 0;
250
+ const workspaceProjectionItem = isWorkspaceScopedConfiguration({
251
+ workspaceId,
252
+ userId
253
+ }) ? buildConfigurationWorkspaceProjectionItem({
254
+ tenantId,
255
+ workspaceId,
256
+ configurationId: id,
257
+ key,
258
+ summary,
259
+ vid,
260
+ lastUpdated
261
+ }) : void 0;
262
+ const canonicalItem = {
151
263
  tenantId,
152
264
  workspaceId,
153
265
  userId,
@@ -159,7 +271,25 @@ async function createConfigurationOperation(params) {
159
271
  vid,
160
272
  lastUpdated,
161
273
  sk: SK
162
- }).go();
274
+ };
275
+ const triples = [
276
+ { entity: "configuration", action: "put", item: canonicalItem }
277
+ ];
278
+ if (userProjectionItem) {
279
+ triples.push({
280
+ entity: "configurationUserProjection",
281
+ action: "put",
282
+ item: userProjectionItem
283
+ });
284
+ }
285
+ if (workspaceProjectionItem) {
286
+ triples.push({
287
+ entity: "configurationWorkspaceProjection",
288
+ action: "put",
289
+ item: workspaceProjectionItem
290
+ });
291
+ }
292
+ await executeMultiWrite({ service, triples });
163
293
  const resource = typeof resourcePayload === "object" ? resourcePayload : JSON.parse(resourceStr);
164
294
  return {
165
295
  id,
@@ -464,7 +594,7 @@ async function deleteConfigurationOperation(params) {
464
594
  const { tenantId, workspaceId, actorId, roleId: ctxRoleId } = context;
465
595
  const roleId = ctxRoleId ?? "-";
466
596
  const service = getDynamoControlService(tableName);
467
- await service.entities.configuration.delete({
597
+ const existing = await service.entities.configuration.get({
468
598
  tenantId,
469
599
  workspaceId,
470
600
  userId: actorId,
@@ -472,6 +602,81 @@ async function deleteConfigurationOperation(params) {
472
602
  key: id,
473
603
  sk: SK2
474
604
  }).go();
605
+ if (!existing.data) {
606
+ return;
607
+ }
608
+ const canonicalUserId = existing.data.userId;
609
+ const canonicalWorkspaceId = existing.data.workspaceId;
610
+ const canonicalKey = existing.data.key;
611
+ const canonicalId = existing.data.id;
612
+ const userProjectionItem = isUserScopedConfiguration(canonicalUserId) ? buildConfigurationUserProjectionItem({
613
+ tenantId: existing.data.tenantId,
614
+ userId: canonicalUserId,
615
+ configurationId: canonicalId,
616
+ key: canonicalKey,
617
+ // The placeholder summary / vid / lastUpdated values are
618
+ // unused by the delete path — ElectroDB only needs the
619
+ // composite key fields (`userId` + `sk`) to issue the
620
+ // DeleteItem. Supplying them keeps the entity-validation
621
+ // pass happy on a `put`-shaped item if the helper ever
622
+ // rejects a sparse delete payload.
623
+ summary: "",
624
+ vid: "",
625
+ lastUpdated: ""
626
+ }) : void 0;
627
+ const workspaceProjectionItem = isWorkspaceScopedConfiguration({
628
+ workspaceId: canonicalWorkspaceId,
629
+ userId: canonicalUserId
630
+ }) ? buildConfigurationWorkspaceProjectionItem({
631
+ tenantId: existing.data.tenantId,
632
+ workspaceId: canonicalWorkspaceId,
633
+ configurationId: canonicalId,
634
+ key: canonicalKey,
635
+ // The placeholder summary / vid / lastUpdated values are
636
+ // unused by the delete path — ElectroDB only needs the
637
+ // composite key fields (`tenantId` + `workspaceId` + `sk`) to
638
+ // issue the DeleteItem. Supplying them keeps entity-validation
639
+ // happy if the helper ever rejects a sparse delete payload.
640
+ summary: "",
641
+ vid: "",
642
+ lastUpdated: ""
643
+ }) : void 0;
644
+ const triples = [
645
+ {
646
+ entity: "configuration",
647
+ action: "delete",
648
+ item: {
649
+ tenantId,
650
+ workspaceId,
651
+ userId: actorId,
652
+ roleId,
653
+ key: id,
654
+ sk: SK2
655
+ }
656
+ }
657
+ ];
658
+ if (userProjectionItem) {
659
+ triples.push({
660
+ entity: "configurationUserProjection",
661
+ action: "delete",
662
+ item: {
663
+ userId: userProjectionItem.userId,
664
+ sk: userProjectionItem.sk
665
+ }
666
+ });
667
+ }
668
+ if (workspaceProjectionItem) {
669
+ triples.push({
670
+ entity: "configurationWorkspaceProjection",
671
+ action: "delete",
672
+ item: {
673
+ tenantId: workspaceProjectionItem.tenantId,
674
+ workspaceId: workspaceProjectionItem.workspaceId,
675
+ sk: workspaceProjectionItem.sk
676
+ }
677
+ });
678
+ }
679
+ await executeMultiWrite({ service, triples });
475
680
  }
476
681
 
477
682
  // src/data/rest-api/routes/control/configuration/configuration-delete-route.ts
@@ -950,6 +1155,47 @@ async function updateConfigurationOperation(params) {
950
1155
  lastUpdated,
951
1156
  vid: nextVid
952
1157
  }).go();
1158
+ const canonicalUserId = existing.data.userId;
1159
+ const canonicalWorkspaceId = existing.data.workspaceId;
1160
+ const userProjectionItem = isUserScopedConfiguration(canonicalUserId) ? buildConfigurationUserProjectionItem({
1161
+ tenantId,
1162
+ userId: canonicalUserId,
1163
+ configurationId: existing.data.id,
1164
+ key: existing.data.key,
1165
+ summary,
1166
+ vid: nextVid,
1167
+ lastUpdated
1168
+ }) : void 0;
1169
+ const workspaceProjectionItem = isWorkspaceScopedConfiguration({
1170
+ workspaceId: canonicalWorkspaceId,
1171
+ userId: canonicalUserId
1172
+ }) ? buildConfigurationWorkspaceProjectionItem({
1173
+ tenantId,
1174
+ workspaceId: canonicalWorkspaceId,
1175
+ configurationId: existing.data.id,
1176
+ key: existing.data.key,
1177
+ summary,
1178
+ vid: nextVid,
1179
+ lastUpdated
1180
+ }) : void 0;
1181
+ const refreshTriples = [];
1182
+ if (userProjectionItem) {
1183
+ refreshTriples.push({
1184
+ entity: "configurationUserProjection",
1185
+ action: "put",
1186
+ item: userProjectionItem
1187
+ });
1188
+ }
1189
+ if (workspaceProjectionItem) {
1190
+ refreshTriples.push({
1191
+ entity: "configurationWorkspaceProjection",
1192
+ action: "put",
1193
+ item: workspaceProjectionItem
1194
+ });
1195
+ }
1196
+ if (refreshTriples.length > 0) {
1197
+ await executeMultiWrite({ service, triples: refreshTriples });
1198
+ }
953
1199
  const parsedResource = typeof resourcePayload === "object" ? resourcePayload : JSON.parse(resourceStr);
954
1200
  return {
955
1201
  id: existing.data.id,
@@ -1384,7 +1630,78 @@ async function createMembershipRoute(req, res) {
1384
1630
  async function deleteMembershipOperation(params) {
1385
1631
  const { context, id, tableName } = params;
1386
1632
  const service = getDynamoControlService(tableName);
1387
- await service.entities.membership.delete({ tenantId: context.tenantId, id, sk: "CURRENT" }).go();
1633
+ const existing = await service.entities.membership.get({ tenantId: context.tenantId, id, sk: "CURRENT" }).go();
1634
+ if (!existing.data) {
1635
+ return;
1636
+ }
1637
+ let parsed;
1638
+ try {
1639
+ parsed = typeof existing.data.resource === "string" ? JSON.parse(existing.data.resource) : void 0;
1640
+ } catch {
1641
+ parsed = void 0;
1642
+ }
1643
+ const userIdFromResource = parsed !== void 0 ? extractReferenceSlug(parsed, "user") : void 0;
1644
+ const workspaceIdFromResource = parsed !== void 0 ? extractReferenceSlug(parsed, "workspace") : void 0;
1645
+ const userProjectionItem = userIdFromResource !== void 0 ? buildMembershipUserProjectionItem({
1646
+ tenantId: context.tenantId,
1647
+ userId: userIdFromResource,
1648
+ workspaceId: workspaceIdFromResource,
1649
+ membershipId: id,
1650
+ // The placeholder summary / vid / lastUpdated values are
1651
+ // unused by the delete path — ElectroDB only needs the
1652
+ // composite key fields (`userId` + `sk`) to issue the
1653
+ // DeleteItem. Supplying them keeps the entity-validation
1654
+ // pass happy on a `put`-shaped item if the helper ever
1655
+ // rejects a sparse delete payload.
1656
+ summary: "",
1657
+ vid: "",
1658
+ lastUpdated: "",
1659
+ denormalizedTenantName: existing.data.denormalizedTenantName,
1660
+ denormalizedUserName: existing.data.denormalizedUserName,
1661
+ // The canonical row does not carry a workspace display name
1662
+ // (TR-024 § Open Item #4); the SK builder falls back to the
1663
+ // missing-name sentinel for the workspace-lane shape.
1664
+ denormalizedWorkspaceName: void 0
1665
+ }) : void 0;
1666
+ const workspaceProjectionItem = userIdFromResource !== void 0 && workspaceIdFromResource !== void 0 ? buildMembershipWorkspaceProjectionItem({
1667
+ tenantId: context.tenantId,
1668
+ workspaceId: workspaceIdFromResource,
1669
+ userId: userIdFromResource,
1670
+ membershipId: id,
1671
+ summary: "",
1672
+ vid: "",
1673
+ lastUpdated: "",
1674
+ denormalizedUserName: existing.data.denormalizedUserName
1675
+ }) : void 0;
1676
+ const triples = [
1677
+ {
1678
+ entity: "membership",
1679
+ action: "delete",
1680
+ item: { tenantId: context.tenantId, id, sk: "CURRENT" }
1681
+ }
1682
+ ];
1683
+ if (userProjectionItem) {
1684
+ triples.push({
1685
+ entity: "membershipUserProjection",
1686
+ action: "delete",
1687
+ item: {
1688
+ userId: userProjectionItem.userId,
1689
+ sk: userProjectionItem.sk
1690
+ }
1691
+ });
1692
+ }
1693
+ if (workspaceProjectionItem) {
1694
+ triples.push({
1695
+ entity: "membershipWorkspaceProjection",
1696
+ action: "delete",
1697
+ item: {
1698
+ tenantId: workspaceProjectionItem.tenantId,
1699
+ workspaceId: workspaceProjectionItem.workspaceId,
1700
+ sk: workspaceProjectionItem.sk
1701
+ }
1702
+ });
1703
+ }
1704
+ await executeMultiWrite({ service, triples });
1388
1705
  }
1389
1706
 
1390
1707
  // src/data/rest-api/routes/control/membership/membership-delete-route.ts
@@ -1476,16 +1793,76 @@ async function updateMembershipOperation(params) {
1476
1793
  }
1477
1794
  throw e;
1478
1795
  }
1796
+ const resourceRecord = resource;
1797
+ const denormalizedTenantName = extractDenormalizedReferenceDisplay(
1798
+ resourceRecord,
1799
+ "tenant"
1800
+ );
1801
+ const denormalizedUserName = extractDenormalizedReferenceDisplay(
1802
+ resourceRecord,
1803
+ "user"
1804
+ );
1805
+ const denormalizedWorkspaceName = extractDenormalizedReferenceDisplay(
1806
+ resourceRecord,
1807
+ "workspace"
1808
+ );
1479
1809
  const summary = JSON.stringify(extractSummary(resource));
1480
- await service.entities.membership.put({
1810
+ const userIdFromResource = extractReferenceSlug(resourceRecord, "user");
1811
+ const workspaceIdFromResource = extractReferenceSlug(
1812
+ resourceRecord,
1813
+ "workspace"
1814
+ );
1815
+ const userProjectionItem = userIdFromResource !== void 0 ? buildMembershipUserProjectionItem({
1816
+ tenantId: context.tenantId,
1817
+ userId: userIdFromResource,
1818
+ workspaceId: workspaceIdFromResource,
1819
+ membershipId: id,
1820
+ summary,
1821
+ vid,
1822
+ lastUpdated,
1823
+ denormalizedTenantName,
1824
+ denormalizedUserName,
1825
+ denormalizedWorkspaceName
1826
+ }) : void 0;
1827
+ const workspaceProjectionItem = userIdFromResource !== void 0 && workspaceIdFromResource !== void 0 ? buildMembershipWorkspaceProjectionItem({
1828
+ tenantId: context.tenantId,
1829
+ workspaceId: workspaceIdFromResource,
1830
+ userId: userIdFromResource,
1831
+ membershipId: id,
1832
+ summary,
1833
+ vid,
1834
+ lastUpdated,
1835
+ denormalizedUserName
1836
+ }) : void 0;
1837
+ const canonicalItem = {
1481
1838
  tenantId: context.tenantId,
1482
1839
  id,
1483
1840
  resource: JSON.stringify(resource),
1484
1841
  summary,
1485
1842
  vid,
1486
1843
  lastUpdated,
1487
- linkedDataIdentityRef
1488
- }).go();
1844
+ linkedDataIdentityRef,
1845
+ denormalizedTenantName,
1846
+ denormalizedUserName
1847
+ };
1848
+ const triples = [
1849
+ { entity: "membership", action: "put", item: canonicalItem }
1850
+ ];
1851
+ if (userProjectionItem) {
1852
+ triples.push({
1853
+ entity: "membershipUserProjection",
1854
+ action: "put",
1855
+ item: userProjectionItem
1856
+ });
1857
+ }
1858
+ if (workspaceProjectionItem) {
1859
+ triples.push({
1860
+ entity: "membershipWorkspaceProjection",
1861
+ action: "put",
1862
+ item: workspaceProjectionItem
1863
+ });
1864
+ }
1865
+ await executeMultiWrite({ service, triples });
1489
1866
  return {
1490
1867
  id,
1491
1868
  resource,
@@ -1781,7 +2158,79 @@ async function createRoleAssignmentRoute(req, res) {
1781
2158
  async function deleteRoleAssignmentOperation(params) {
1782
2159
  const { context, id, tableName } = params;
1783
2160
  const service = getDynamoControlService(tableName);
1784
- await service.entities.roleAssignment.delete({ tenantId: context.tenantId, id, sk: "CURRENT" }).go();
2161
+ const existing = await service.entities.roleAssignment.get({ tenantId: context.tenantId, id, sk: "CURRENT" }).go();
2162
+ if (!existing.data) {
2163
+ return;
2164
+ }
2165
+ let parsed;
2166
+ try {
2167
+ parsed = typeof existing.data.resource === "string" ? JSON.parse(existing.data.resource) : void 0;
2168
+ } catch {
2169
+ parsed = void 0;
2170
+ }
2171
+ const userIdFromResource = parsed !== void 0 ? extractReferenceSlug2(parsed, "user") : void 0;
2172
+ const roleIdFromResource = parsed !== void 0 ? extractReferenceSlug2(parsed, "role") : void 0;
2173
+ const workspaceIdFromResource = parsed !== void 0 ? extractReferenceSlug2(parsed, "workspace") : void 0;
2174
+ const userProjectionItem = userIdFromResource !== void 0 && roleIdFromResource !== void 0 ? buildRoleAssignmentUserProjectionItem({
2175
+ tenantId: context.tenantId,
2176
+ userId: userIdFromResource,
2177
+ workspaceId: workspaceIdFromResource,
2178
+ roleId: roleIdFromResource,
2179
+ roleAssignmentId: id,
2180
+ // The placeholder summary / vid / lastUpdated values are
2181
+ // unused by the delete path — ElectroDB only needs the
2182
+ // composite key fields (`userId` + `sk`) to issue the
2183
+ // DeleteItem. Supplying them keeps the entity-validation
2184
+ // pass happy on a `put`-shaped item if the helper ever
2185
+ // rejects a sparse delete payload.
2186
+ summary: "",
2187
+ vid: "",
2188
+ lastUpdated: "",
2189
+ denormalizedTenantName: existing.data.denormalizedTenantName,
2190
+ denormalizedUserName: existing.data.denormalizedUserName,
2191
+ denormalizedRoleName: existing.data.denormalizedRoleName
2192
+ }) : void 0;
2193
+ const workspaceProjectionItem = userIdFromResource !== void 0 && roleIdFromResource !== void 0 && workspaceIdFromResource !== void 0 ? buildRoleAssignmentWorkspaceProjectionItem({
2194
+ tenantId: context.tenantId,
2195
+ workspaceId: workspaceIdFromResource,
2196
+ userId: userIdFromResource,
2197
+ roleId: roleIdFromResource,
2198
+ roleAssignmentId: id,
2199
+ summary: "",
2200
+ vid: "",
2201
+ lastUpdated: "",
2202
+ denormalizedUserName: existing.data.denormalizedUserName,
2203
+ denormalizedRoleName: existing.data.denormalizedRoleName
2204
+ }) : void 0;
2205
+ const triples = [
2206
+ {
2207
+ entity: "roleAssignment",
2208
+ action: "delete",
2209
+ item: { tenantId: context.tenantId, id, sk: "CURRENT" }
2210
+ }
2211
+ ];
2212
+ if (userProjectionItem) {
2213
+ triples.push({
2214
+ entity: "roleAssignmentUserProjection",
2215
+ action: "delete",
2216
+ item: {
2217
+ userId: userProjectionItem.userId,
2218
+ sk: userProjectionItem.sk
2219
+ }
2220
+ });
2221
+ }
2222
+ if (workspaceProjectionItem) {
2223
+ triples.push({
2224
+ entity: "roleAssignmentWorkspaceProjection",
2225
+ action: "delete",
2226
+ item: {
2227
+ tenantId: workspaceProjectionItem.tenantId,
2228
+ workspaceId: workspaceProjectionItem.workspaceId,
2229
+ sk: workspaceProjectionItem.sk
2230
+ }
2231
+ });
2232
+ }
2233
+ await executeMultiWrite({ service, triples });
1785
2234
  }
1786
2235
 
1787
2236
  // src/data/rest-api/routes/control/roleassignment/roleassignment-delete-route.ts
@@ -1873,15 +2322,80 @@ async function updateRoleAssignmentOperation(params) {
1873
2322
  const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
1874
2323
  const vid = `${Date.now()}`;
1875
2324
  const resource = { resourceType: "RoleAssignment", id, ...parsedResource };
2325
+ const resourceRecord = resource;
2326
+ const denormalizedTenantName = extractDenormalizedReferenceDisplay(
2327
+ resourceRecord,
2328
+ "tenant"
2329
+ );
2330
+ const denormalizedUserName = extractDenormalizedReferenceDisplay(
2331
+ resourceRecord,
2332
+ "user"
2333
+ );
2334
+ const denormalizedRoleName = extractDenormalizedReferenceDisplay(
2335
+ resourceRecord,
2336
+ "role"
2337
+ );
1876
2338
  const summary = JSON.stringify(extractSummary3(resource));
1877
- await service.entities.roleAssignment.put({
2339
+ const userIdFromResource = extractReferenceSlug2(resourceRecord, "user");
2340
+ const roleIdFromResource = extractReferenceSlug2(resourceRecord, "role");
2341
+ const workspaceIdFromResource = extractReferenceSlug2(
2342
+ resourceRecord,
2343
+ "workspace"
2344
+ );
2345
+ const userProjectionItem = userIdFromResource !== void 0 && roleIdFromResource !== void 0 ? buildRoleAssignmentUserProjectionItem({
2346
+ tenantId: context.tenantId,
2347
+ userId: userIdFromResource,
2348
+ workspaceId: workspaceIdFromResource,
2349
+ roleId: roleIdFromResource,
2350
+ roleAssignmentId: id,
2351
+ summary,
2352
+ vid,
2353
+ lastUpdated,
2354
+ denormalizedTenantName,
2355
+ denormalizedUserName,
2356
+ denormalizedRoleName
2357
+ }) : void 0;
2358
+ const workspaceProjectionItem = userIdFromResource !== void 0 && roleIdFromResource !== void 0 && workspaceIdFromResource !== void 0 ? buildRoleAssignmentWorkspaceProjectionItem({
2359
+ tenantId: context.tenantId,
2360
+ workspaceId: workspaceIdFromResource,
2361
+ userId: userIdFromResource,
2362
+ roleId: roleIdFromResource,
2363
+ roleAssignmentId: id,
2364
+ summary,
2365
+ vid,
2366
+ lastUpdated,
2367
+ denormalizedUserName,
2368
+ denormalizedRoleName
2369
+ }) : void 0;
2370
+ const canonicalItem = {
1878
2371
  tenantId: context.tenantId,
1879
2372
  id,
1880
2373
  resource: JSON.stringify(resource),
1881
2374
  summary,
1882
2375
  vid,
1883
- lastUpdated
1884
- }).go();
2376
+ lastUpdated,
2377
+ denormalizedTenantName,
2378
+ denormalizedUserName,
2379
+ denormalizedRoleName
2380
+ };
2381
+ const triples = [
2382
+ { entity: "roleAssignment", action: "put", item: canonicalItem }
2383
+ ];
2384
+ if (userProjectionItem) {
2385
+ triples.push({
2386
+ entity: "roleAssignmentUserProjection",
2387
+ action: "put",
2388
+ item: userProjectionItem
2389
+ });
2390
+ }
2391
+ if (workspaceProjectionItem) {
2392
+ triples.push({
2393
+ entity: "roleAssignmentWorkspaceProjection",
2394
+ action: "put",
2395
+ item: workspaceProjectionItem
2396
+ });
2397
+ }
2398
+ await executeMultiWrite({ service, triples });
1885
2399
  return {
1886
2400
  id,
1887
2401
  resource,
@@ -2236,64 +2750,827 @@ async function getUserByIdRoute(req, res) {
2236
2750
  }
2237
2751
  }
2238
2752
 
2239
- // src/data/rest-api/routes/control/user/user-list-route.ts
2240
- async function listUsersRoute(req, res) {
2241
- return handleListRoute({
2242
- req,
2243
- res,
2244
- basePath: BASE_PATH.USER,
2245
- listOperation: listUsersOperation,
2246
- errorLogContext: "GET /User list error:"
2247
- });
2753
+ // src/data/operations/control/configuration/configuration-list-by-user-operation.ts
2754
+ async function configurationListByUserOperation(params) {
2755
+ const { userId, cursor = null, limit, order, tableName } = params;
2756
+ const service = getDynamoControlService(tableName);
2757
+ const goOptions = {
2758
+ cursor
2759
+ };
2760
+ if (limit !== void 0) {
2761
+ goOptions.limit = limit;
2762
+ }
2763
+ if (order !== void 0) {
2764
+ goOptions.order = order;
2765
+ }
2766
+ const result = await service.entities.configurationUserProjection.query.record({ userId }).begins({ sk: "CONFIGURATION#" }).go(goOptions);
2767
+ const items = (result.data ?? []).map((row) => ({
2768
+ userId: row.userId,
2769
+ sk: row.sk,
2770
+ tenantId: row.tenantId,
2771
+ configurationId: row.configurationId,
2772
+ scope: "user",
2773
+ displayName: row.displayName,
2774
+ summary: row.summary,
2775
+ vid: row.vid,
2776
+ lastUpdated: row.lastUpdated
2777
+ }));
2778
+ return { items, cursor: result.cursor ?? null };
2248
2779
  }
2249
2780
 
2250
- // src/data/rest-api/routes/control/user/user-update-route.ts
2251
- async function updateUserRoute(req, res) {
2252
- const bodyResult = requireJsonBody(req, res);
2253
- if ("errorResponse" in bodyResult) return bodyResult.errorResponse;
2254
- const id = String(req.params.id);
2255
- const ctx = req.openhiContext;
2256
- const body = bodyResult.body;
2257
- try {
2258
- const result = await updateUserOperation({
2259
- context: ctx,
2260
- id,
2261
- body: {
2262
- resource: body?.resource
2263
- }
2264
- });
2265
- return res.json({ ...result.resource, meta: result.meta });
2266
- } catch (err) {
2267
- const status = domainErrorToHttpStatus(err);
2268
- if (status === 404) {
2269
- return res.status(404).json({
2270
- resourceType: "OperationOutcome",
2271
- issue: [
2272
- {
2273
- severity: "error",
2274
- code: "not-found",
2275
- diagnostics: err instanceof NotFoundError ? err.message : `User ${id} not found`
2276
- }
2277
- ]
2278
- });
2781
+ // src/data/operations/control/configuration/configuration-list-by-workspace-operation.ts
2782
+ async function configurationListByWorkspaceOperation(params) {
2783
+ const {
2784
+ tenantId,
2785
+ workspaceId,
2786
+ cursor = null,
2787
+ limit,
2788
+ order,
2789
+ tableName
2790
+ } = params;
2791
+ const service = getDynamoControlService(tableName);
2792
+ const goOptions = {
2793
+ cursor
2794
+ };
2795
+ if (limit !== void 0) {
2796
+ goOptions.limit = limit;
2797
+ }
2798
+ if (order !== void 0) {
2799
+ goOptions.order = order;
2800
+ }
2801
+ const result = await service.entities.configurationWorkspaceProjection.query.record({ tenantId, workspaceId }).begins({ sk: "CONFIGURATION#" }).go(goOptions);
2802
+ const items = (result.data ?? []).map((row) => ({
2803
+ tenantId: row.tenantId,
2804
+ workspaceId: row.workspaceId,
2805
+ sk: row.sk,
2806
+ configurationId: row.configurationId,
2807
+ scope: "workspace",
2808
+ displayName: row.displayName,
2809
+ summary: row.summary,
2810
+ vid: row.vid,
2811
+ lastUpdated: row.lastUpdated
2812
+ }));
2813
+ return { items, cursor: result.cursor ?? null };
2814
+ }
2815
+
2816
+ // src/data/rest-api/routes/control/cross-cutting-route-helpers.ts
2817
+ function sendInvalidQuery400(res, diagnostics) {
2818
+ return res.status(400).json({
2819
+ resourceType: "OperationOutcome",
2820
+ issue: [{ severity: "error", code: "invalid", diagnostics }]
2821
+ });
2822
+ }
2823
+ function sendForbidden403(res, diagnostics) {
2824
+ return res.status(403).json({
2825
+ resourceType: "OperationOutcome",
2826
+ issue: [{ severity: "error", code: "forbidden", diagnostics }]
2827
+ });
2828
+ }
2829
+ var COMMON_KEYS = ["cursor", "limit", "order"];
2830
+ function parseCommonListQuery(req, res, options = {}) {
2831
+ const q = req.query ?? {};
2832
+ const allowedKeys = /* @__PURE__ */ new Set([
2833
+ ...COMMON_KEYS,
2834
+ ...options.extraKeys ?? []
2835
+ ]);
2836
+ const extra = Object.keys(q).filter((k) => !allowedKeys.has(k));
2837
+ if (extra.length > 0) {
2838
+ return {
2839
+ ok: false,
2840
+ response: sendInvalidQuery400(
2841
+ res,
2842
+ `Unsupported query parameter${extra.length === 1 ? "" : "s"}: ${extra.join(", ")}.`
2843
+ )
2844
+ };
2845
+ }
2846
+ const rawCursor = q.cursor;
2847
+ let cursor = null;
2848
+ if (rawCursor !== void 0) {
2849
+ if (typeof rawCursor !== "string") {
2850
+ return {
2851
+ ok: false,
2852
+ response: sendInvalidQuery400(
2853
+ res,
2854
+ "Query parameter `cursor` must be a string."
2855
+ )
2856
+ };
2279
2857
  }
2280
- console.error("PUT User error:", err);
2281
- return res.status(500).json({
2282
- resourceType: "OperationOutcome",
2283
- issue: [
2284
- { severity: "error", code: "exception", diagnostics: String(err) }
2285
- ]
2858
+ cursor = rawCursor;
2859
+ }
2860
+ const rawLimit = q.limit;
2861
+ let limit;
2862
+ if (rawLimit !== void 0) {
2863
+ if (typeof rawLimit !== "string" || rawLimit.length === 0) {
2864
+ return {
2865
+ ok: false,
2866
+ response: sendInvalidQuery400(
2867
+ res,
2868
+ "Query parameter `limit` must be a positive integer."
2869
+ )
2870
+ };
2871
+ }
2872
+ const parsed = Number(rawLimit);
2873
+ if (!Number.isInteger(parsed) || parsed <= 0) {
2874
+ return {
2875
+ ok: false,
2876
+ response: sendInvalidQuery400(
2877
+ res,
2878
+ "Query parameter `limit` must be a positive integer."
2879
+ )
2880
+ };
2881
+ }
2882
+ limit = parsed;
2883
+ }
2884
+ const rawOrder = q.order;
2885
+ let order;
2886
+ if (rawOrder !== void 0) {
2887
+ if (rawOrder !== "asc" && rawOrder !== "desc") {
2888
+ return {
2889
+ ok: false,
2890
+ response: sendInvalidQuery400(
2891
+ res,
2892
+ 'Query parameter `order` must be one of "asc", "desc".'
2893
+ )
2894
+ };
2895
+ }
2896
+ order = rawOrder;
2897
+ }
2898
+ const value = order === void 0 ? limit === void 0 ? { cursor } : { cursor, limit } : limit === void 0 ? { cursor, order } : { cursor, limit, order };
2899
+ return { ok: true, value };
2900
+ }
2901
+ function buildPaginationLinks(opts) {
2902
+ const links = [
2903
+ { relation: "self", url: composeUrl(opts.basePath, opts.query) }
2904
+ ];
2905
+ if (opts.nextCursor !== null && opts.nextCursor.length > 0) {
2906
+ const nextQuery = { ...opts.query };
2907
+ nextQuery.cursor = opts.nextCursor;
2908
+ links.push({
2909
+ relation: "next",
2910
+ url: composeUrl(opts.basePath, nextQuery)
2286
2911
  });
2287
2912
  }
2913
+ return links;
2914
+ }
2915
+ function composeUrl(basePath, query) {
2916
+ const usp = new URLSearchParams();
2917
+ for (const [key, value] of Object.entries(query)) {
2918
+ if (value === void 0 || value === null) {
2919
+ continue;
2920
+ }
2921
+ if (Array.isArray(value)) {
2922
+ for (const v of value) {
2923
+ if (v !== void 0 && v !== null) {
2924
+ usp.append(key, String(v));
2925
+ }
2926
+ }
2927
+ } else {
2928
+ usp.append(key, String(value));
2929
+ }
2930
+ }
2931
+ const qs = usp.toString();
2932
+ return qs.length === 0 ? basePath : `${basePath}?${qs}`;
2288
2933
  }
2289
2934
 
2290
- // src/data/rest-api/routes/control/user/user.ts
2291
- var router6 = express6.Router();
2292
- router6.get("/", listUsersRoute);
2293
- router6.get("/:id", getUserByIdRoute);
2294
- router6.post("/", createUserRoute);
2295
- router6.put("/:id", updateUserRoute);
2296
- router6.delete("/:id", deleteUserRoute);
2935
+ // src/data/rest-api/routes/control/projection-bundle-helpers.ts
2936
+ var EXT_BASE = "https://openhi.org/fhir/StructureDefinition";
2937
+ function parseProjectionSummary(summary) {
2938
+ if (typeof summary !== "string" || summary.length === 0) {
2939
+ return {};
2940
+ }
2941
+ try {
2942
+ const parsed = JSON.parse(summary);
2943
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
2944
+ return parsed;
2945
+ }
2946
+ } catch {
2947
+ }
2948
+ return {};
2949
+ }
2950
+ function maybeStringExt(url, value) {
2951
+ if (typeof value !== "string" || value.length === 0) {
2952
+ return void 0;
2953
+ }
2954
+ return { url, valueString: value };
2955
+ }
2956
+ function buildMembershipUserProjectionEntryResource(row) {
2957
+ const extensions = [
2958
+ { url: `${EXT_BASE}/projection-tenant-id`, valueString: row.tenantId }
2959
+ ];
2960
+ const workspaceExt = maybeStringExt(
2961
+ `${EXT_BASE}/projection-workspace-id`,
2962
+ row.workspaceId
2963
+ );
2964
+ if (workspaceExt) {
2965
+ extensions.push(workspaceExt);
2966
+ }
2967
+ const tenantNameExt = maybeStringExt(
2968
+ `${EXT_BASE}/projection-denormalized-tenant-name`,
2969
+ row.denormalizedTenantName
2970
+ );
2971
+ if (tenantNameExt) {
2972
+ extensions.push(tenantNameExt);
2973
+ }
2974
+ const userNameExt = maybeStringExt(
2975
+ `${EXT_BASE}/projection-denormalized-user-name`,
2976
+ row.denormalizedUserName
2977
+ );
2978
+ if (userNameExt) {
2979
+ extensions.push(userNameExt);
2980
+ }
2981
+ const workspaceNameExt = maybeStringExt(
2982
+ `${EXT_BASE}/projection-denormalized-workspace-name`,
2983
+ row.denormalizedWorkspaceName
2984
+ );
2985
+ if (workspaceNameExt) {
2986
+ extensions.push(workspaceNameExt);
2987
+ }
2988
+ return {
2989
+ resourceType: "Membership",
2990
+ id: row.membershipId,
2991
+ ...parseProjectionSummary(row.summary),
2992
+ meta: { versionId: row.vid, lastUpdated: row.lastUpdated },
2993
+ extension: extensions
2994
+ };
2995
+ }
2996
+ function buildMembershipWorkspaceProjectionEntryResource(row) {
2997
+ const extensions = [
2998
+ { url: `${EXT_BASE}/projection-tenant-id`, valueString: row.tenantId },
2999
+ {
3000
+ url: `${EXT_BASE}/projection-workspace-id`,
3001
+ valueString: row.workspaceId
3002
+ }
3003
+ ];
3004
+ const userNameExt = maybeStringExt(
3005
+ `${EXT_BASE}/projection-denormalized-user-name`,
3006
+ row.denormalizedUserName
3007
+ );
3008
+ if (userNameExt) {
3009
+ extensions.push(userNameExt);
3010
+ }
3011
+ return {
3012
+ resourceType: "Membership",
3013
+ id: row.membershipId,
3014
+ ...parseProjectionSummary(row.summary),
3015
+ meta: { versionId: row.vid, lastUpdated: row.lastUpdated },
3016
+ extension: extensions
3017
+ };
3018
+ }
3019
+ function buildRoleAssignmentUserProjectionEntryResource(row) {
3020
+ const extensions = [
3021
+ { url: `${EXT_BASE}/projection-tenant-id`, valueString: row.tenantId }
3022
+ ];
3023
+ const workspaceExt = maybeStringExt(
3024
+ `${EXT_BASE}/projection-workspace-id`,
3025
+ row.workspaceId
3026
+ );
3027
+ if (workspaceExt) {
3028
+ extensions.push(workspaceExt);
3029
+ }
3030
+ extensions.push({
3031
+ url: `${EXT_BASE}/projection-role-id`,
3032
+ valueString: row.roleId
3033
+ });
3034
+ const tenantNameExt = maybeStringExt(
3035
+ `${EXT_BASE}/projection-denormalized-tenant-name`,
3036
+ row.denormalizedTenantName
3037
+ );
3038
+ if (tenantNameExt) {
3039
+ extensions.push(tenantNameExt);
3040
+ }
3041
+ const userNameExt = maybeStringExt(
3042
+ `${EXT_BASE}/projection-denormalized-user-name`,
3043
+ row.denormalizedUserName
3044
+ );
3045
+ if (userNameExt) {
3046
+ extensions.push(userNameExt);
3047
+ }
3048
+ const roleNameExt = maybeStringExt(
3049
+ `${EXT_BASE}/projection-denormalized-role-name`,
3050
+ row.denormalizedRoleName
3051
+ );
3052
+ if (roleNameExt) {
3053
+ extensions.push(roleNameExt);
3054
+ }
3055
+ return {
3056
+ resourceType: "RoleAssignment",
3057
+ id: row.roleAssignmentId,
3058
+ ...parseProjectionSummary(row.summary),
3059
+ meta: { versionId: row.vid, lastUpdated: row.lastUpdated },
3060
+ extension: extensions
3061
+ };
3062
+ }
3063
+ function buildRoleAssignmentWorkspaceProjectionEntryResource(row) {
3064
+ const extensions = [
3065
+ { url: `${EXT_BASE}/projection-tenant-id`, valueString: row.tenantId },
3066
+ {
3067
+ url: `${EXT_BASE}/projection-workspace-id`,
3068
+ valueString: row.workspaceId
3069
+ },
3070
+ { url: `${EXT_BASE}/projection-role-id`, valueString: row.roleId }
3071
+ ];
3072
+ const userNameExt = maybeStringExt(
3073
+ `${EXT_BASE}/projection-denormalized-user-name`,
3074
+ row.denormalizedUserName
3075
+ );
3076
+ if (userNameExt) {
3077
+ extensions.push(userNameExt);
3078
+ }
3079
+ const roleNameExt = maybeStringExt(
3080
+ `${EXT_BASE}/projection-denormalized-role-name`,
3081
+ row.denormalizedRoleName
3082
+ );
3083
+ if (roleNameExt) {
3084
+ extensions.push(roleNameExt);
3085
+ }
3086
+ return {
3087
+ resourceType: "RoleAssignment",
3088
+ id: row.roleAssignmentId,
3089
+ ...parseProjectionSummary(row.summary),
3090
+ meta: { versionId: row.vid, lastUpdated: row.lastUpdated },
3091
+ extension: extensions
3092
+ };
3093
+ }
3094
+ function buildConfigurationUserProjectionEntryResource(row) {
3095
+ const extensions = [
3096
+ { url: `${EXT_BASE}/projection-tenant-id`, valueString: row.tenantId },
3097
+ { url: `${EXT_BASE}/projection-scope`, valueString: row.scope }
3098
+ ];
3099
+ const displayNameExt = maybeStringExt(
3100
+ `${EXT_BASE}/projection-display-name`,
3101
+ row.displayName
3102
+ );
3103
+ if (displayNameExt) {
3104
+ extensions.push(displayNameExt);
3105
+ }
3106
+ return {
3107
+ resourceType: "Configuration",
3108
+ id: row.configurationId,
3109
+ ...parseProjectionSummary(row.summary),
3110
+ meta: { versionId: row.vid, lastUpdated: row.lastUpdated },
3111
+ extension: extensions
3112
+ };
3113
+ }
3114
+ function buildConfigurationWorkspaceProjectionEntryResource(row) {
3115
+ const extensions = [
3116
+ { url: `${EXT_BASE}/projection-tenant-id`, valueString: row.tenantId },
3117
+ {
3118
+ url: `${EXT_BASE}/projection-workspace-id`,
3119
+ valueString: row.workspaceId
3120
+ },
3121
+ { url: `${EXT_BASE}/projection-scope`, valueString: row.scope }
3122
+ ];
3123
+ const displayNameExt = maybeStringExt(
3124
+ `${EXT_BASE}/projection-display-name`,
3125
+ row.displayName
3126
+ );
3127
+ if (displayNameExt) {
3128
+ extensions.push(displayNameExt);
3129
+ }
3130
+ return {
3131
+ resourceType: "Configuration",
3132
+ id: row.configurationId,
3133
+ ...parseProjectionSummary(row.summary),
3134
+ meta: { versionId: row.vid, lastUpdated: row.lastUpdated },
3135
+ extension: extensions
3136
+ };
3137
+ }
3138
+
3139
+ // src/data/rest-api/routes/control/user/user-list-configurations-route.ts
3140
+ async function listUserConfigurationsRoute(req, res) {
3141
+ const ctx = req.openhiContext;
3142
+ if (!ctx) {
3143
+ return sendForbidden403(
3144
+ res,
3145
+ "Missing or invalid OpenHI JWT claims (tenant, workspace, or audit context)."
3146
+ );
3147
+ }
3148
+ const userId = String(req.params.id);
3149
+ if (userId !== ctx.actorId) {
3150
+ return sendForbidden403(
3151
+ res,
3152
+ "Caller is not authorized to read Configurations for another User."
3153
+ );
3154
+ }
3155
+ const parsed = parseCommonListQuery(req, res);
3156
+ if (!parsed.ok) {
3157
+ return parsed.response;
3158
+ }
3159
+ try {
3160
+ const result = await configurationListByUserOperation({
3161
+ userId,
3162
+ cursor: parsed.value.cursor,
3163
+ ...parsed.value.limit !== void 0 && { limit: parsed.value.limit },
3164
+ ...parsed.value.order !== void 0 && { order: parsed.value.order }
3165
+ });
3166
+ const basePath = `${BASE_PATH.USER}/${userId}/Configuration`;
3167
+ const entries = result.items.map((row) => ({
3168
+ fullUrl: `${BASE_PATH.CONFIGURATION}/${row.configurationId}`,
3169
+ resource: buildConfigurationUserProjectionEntryResource(row)
3170
+ }));
3171
+ return res.json({
3172
+ resourceType: "Bundle",
3173
+ type: "searchset",
3174
+ total: entries.length,
3175
+ link: buildPaginationLinks({
3176
+ basePath,
3177
+ query: req.query,
3178
+ nextCursor: result.cursor
3179
+ }),
3180
+ entry: entries
3181
+ });
3182
+ } catch (err) {
3183
+ return sendOperationOutcome500(
3184
+ res,
3185
+ err,
3186
+ "GET /User/:id/Configuration error:"
3187
+ );
3188
+ }
3189
+ }
3190
+
3191
+ // src/data/operations/control/membership/membership-list-by-workspace-operation.ts
3192
+ async function membershipListByWorkspaceOperation(params) {
3193
+ const {
3194
+ tenantId,
3195
+ workspaceId,
3196
+ cursor = null,
3197
+ limit,
3198
+ order,
3199
+ tableName
3200
+ } = params;
3201
+ const service = getDynamoControlService(tableName);
3202
+ const goOptions = {
3203
+ cursor
3204
+ };
3205
+ if (limit !== void 0) {
3206
+ goOptions.limit = limit;
3207
+ }
3208
+ if (order !== void 0) {
3209
+ goOptions.order = order;
3210
+ }
3211
+ const result = await service.entities.membershipWorkspaceProjection.query.record({ tenantId, workspaceId }).begins({ sk: "MEMBERSHIP#" }).go(goOptions);
3212
+ const items = (result.data ?? []).map((row) => ({
3213
+ tenantId: row.tenantId,
3214
+ workspaceId: row.workspaceId,
3215
+ sk: row.sk,
3216
+ userId: row.userId,
3217
+ membershipId: row.membershipId,
3218
+ summary: row.summary,
3219
+ vid: row.vid,
3220
+ lastUpdated: row.lastUpdated,
3221
+ denormalizedUserName: row.denormalizedUserName
3222
+ }));
3223
+ return { items, cursor: result.cursor ?? null };
3224
+ }
3225
+
3226
+ // src/data/rest-api/routes/control/user/user-list-memberships-route.ts
3227
+ var ALLOWED_MODES = [
3228
+ "all",
3229
+ "tenant",
3230
+ "workspace",
3231
+ "workspaceInTenant"
3232
+ ];
3233
+ async function listUserMembershipsRoute(req, res) {
3234
+ const ctx = req.openhiContext;
3235
+ if (!ctx) {
3236
+ return sendForbidden403(
3237
+ res,
3238
+ "Missing or invalid OpenHI JWT claims (tenant, workspace, or audit context)."
3239
+ );
3240
+ }
3241
+ const userId = String(req.params.id);
3242
+ if (userId !== ctx.actorId) {
3243
+ return sendForbidden403(
3244
+ res,
3245
+ "Caller is not authorized to read Memberships for another User."
3246
+ );
3247
+ }
3248
+ const parsed = parseCommonListQuery(req, res, {
3249
+ extraKeys: ["mode", "tenantId"]
3250
+ });
3251
+ if (!parsed.ok) {
3252
+ return parsed.response;
3253
+ }
3254
+ const rawMode = req.query.mode;
3255
+ let mode = "all";
3256
+ if (rawMode !== void 0) {
3257
+ if (typeof rawMode !== "string" || !ALLOWED_MODES.includes(rawMode)) {
3258
+ return sendInvalidQuery400(
3259
+ res,
3260
+ `Query parameter \`mode\` must be one of: ${ALLOWED_MODES.join(", ")}.`
3261
+ );
3262
+ }
3263
+ mode = rawMode;
3264
+ }
3265
+ const rawTenantId = req.query.tenantId;
3266
+ let tenantId;
3267
+ if (rawTenantId !== void 0) {
3268
+ if (typeof rawTenantId !== "string" || rawTenantId.length === 0) {
3269
+ return sendInvalidQuery400(
3270
+ res,
3271
+ "Query parameter `tenantId` must be a non-empty string."
3272
+ );
3273
+ }
3274
+ tenantId = rawTenantId;
3275
+ }
3276
+ if (mode === "workspaceInTenant" && !tenantId) {
3277
+ return sendInvalidQuery400(
3278
+ res,
3279
+ 'Query parameter `tenantId` is required when `mode === "workspaceInTenant"`.'
3280
+ );
3281
+ }
3282
+ try {
3283
+ const result = await membershipListByUserOperation({
3284
+ userId,
3285
+ mode,
3286
+ tenantId,
3287
+ cursor: parsed.value.cursor,
3288
+ ...parsed.value.limit !== void 0 && { limit: parsed.value.limit },
3289
+ ...parsed.value.order !== void 0 && { order: parsed.value.order }
3290
+ });
3291
+ const basePath = `${BASE_PATH.USER}/${userId}/Membership`;
3292
+ const entries = result.items.map((row) => ({
3293
+ fullUrl: `${BASE_PATH.MEMBERSHIP}/${row.membershipId}`,
3294
+ resource: buildMembershipUserProjectionEntryResource(row)
3295
+ }));
3296
+ return res.json({
3297
+ resourceType: "Bundle",
3298
+ type: "searchset",
3299
+ total: entries.length,
3300
+ link: buildPaginationLinks({
3301
+ basePath,
3302
+ query: req.query,
3303
+ nextCursor: result.cursor
3304
+ }),
3305
+ entry: entries
3306
+ });
3307
+ } catch (err) {
3308
+ return sendOperationOutcome500(res, err, "GET /User/:id/Membership error:");
3309
+ }
3310
+ }
3311
+
3312
+ // src/data/operations/control/roleassignment/roleassignment-list-by-user-operation.ts
3313
+ function buildSkPrefix(mode) {
3314
+ switch (mode) {
3315
+ case "tenant":
3316
+ return "ROLEASSIGNMENT#TENANT#";
3317
+ case "workspace":
3318
+ case "workspaceInTenant":
3319
+ return "ROLEASSIGNMENT#WORKSPACE#";
3320
+ case "all":
3321
+ default:
3322
+ return "ROLEASSIGNMENT#";
3323
+ }
3324
+ }
3325
+ async function roleAssignmentListByUserOperation(params) {
3326
+ const {
3327
+ userId,
3328
+ mode = "all",
3329
+ tenantId,
3330
+ workspaceId,
3331
+ cursor = null,
3332
+ limit,
3333
+ order,
3334
+ tableName
3335
+ } = params;
3336
+ if (mode === "workspaceInTenant" && !tenantId) {
3337
+ throw new Error(
3338
+ 'roleAssignmentListByUserOperation: tenantId is required when mode === "workspaceInTenant"'
3339
+ );
3340
+ }
3341
+ const service = getDynamoControlService(tableName);
3342
+ const skPrefix = buildSkPrefix(mode);
3343
+ const goOptions = {
3344
+ cursor
3345
+ };
3346
+ if (limit !== void 0) {
3347
+ goOptions.limit = limit;
3348
+ }
3349
+ if (order !== void 0) {
3350
+ goOptions.order = order;
3351
+ }
3352
+ const baseQuery = service.entities.roleAssignmentUserProjection.query.record({ userId }).begins({ sk: skPrefix });
3353
+ const filteredQuery = mode === "workspaceInTenant" ? baseQuery.where((attr, op) => {
3354
+ const tenantClause = op.eq(attr.tenantId, tenantId);
3355
+ if (workspaceId === void 0 || workspaceId.length === 0) {
3356
+ return tenantClause;
3357
+ }
3358
+ return `${tenantClause} AND ${op.eq(attr.workspaceId, workspaceId)}`;
3359
+ }) : baseQuery;
3360
+ const result = await filteredQuery.go(goOptions);
3361
+ const items = (result.data ?? []).map((row) => ({
3362
+ userId: row.userId,
3363
+ sk: row.sk,
3364
+ tenantId: row.tenantId,
3365
+ workspaceId: row.workspaceId,
3366
+ roleId: row.roleId,
3367
+ roleAssignmentId: row.roleAssignmentId,
3368
+ summary: row.summary,
3369
+ vid: row.vid,
3370
+ lastUpdated: row.lastUpdated,
3371
+ denormalizedTenantName: row.denormalizedTenantName,
3372
+ denormalizedUserName: row.denormalizedUserName,
3373
+ denormalizedRoleName: row.denormalizedRoleName
3374
+ }));
3375
+ return { items, cursor: result.cursor ?? null };
3376
+ }
3377
+
3378
+ // src/data/operations/control/roleassignment/roleassignment-list-by-workspace-operation.ts
3379
+ function buildSkPrefix2(roleId) {
3380
+ if (roleId === void 0 || roleId.length === 0) {
3381
+ return "ROLEASSIGNMENT#";
3382
+ }
3383
+ return `ROLEASSIGNMENT#${roleId}#`;
3384
+ }
3385
+ async function roleAssignmentListByWorkspaceOperation(params) {
3386
+ const {
3387
+ tenantId,
3388
+ workspaceId,
3389
+ roleId,
3390
+ cursor = null,
3391
+ limit,
3392
+ order,
3393
+ tableName
3394
+ } = params;
3395
+ const service = getDynamoControlService(tableName);
3396
+ const skPrefix = buildSkPrefix2(roleId);
3397
+ const goOptions = {
3398
+ cursor
3399
+ };
3400
+ if (limit !== void 0) {
3401
+ goOptions.limit = limit;
3402
+ }
3403
+ if (order !== void 0) {
3404
+ goOptions.order = order;
3405
+ }
3406
+ const result = await service.entities.roleAssignmentWorkspaceProjection.query.record({ tenantId, workspaceId }).begins({ sk: skPrefix }).go(goOptions);
3407
+ const items = (result.data ?? []).map((row) => ({
3408
+ tenantId: row.tenantId,
3409
+ workspaceId: row.workspaceId,
3410
+ sk: row.sk,
3411
+ userId: row.userId,
3412
+ roleId: row.roleId,
3413
+ roleAssignmentId: row.roleAssignmentId,
3414
+ summary: row.summary,
3415
+ vid: row.vid,
3416
+ lastUpdated: row.lastUpdated,
3417
+ denormalizedUserName: row.denormalizedUserName,
3418
+ denormalizedRoleName: row.denormalizedRoleName
3419
+ }));
3420
+ return { items, cursor: result.cursor ?? null };
3421
+ }
3422
+
3423
+ // src/data/rest-api/routes/control/user/user-list-role-assignments-route.ts
3424
+ var ALLOWED_MODES2 = [
3425
+ "all",
3426
+ "tenant",
3427
+ "workspace",
3428
+ "workspaceInTenant"
3429
+ ];
3430
+ async function listUserRoleAssignmentsRoute(req, res) {
3431
+ const ctx = req.openhiContext;
3432
+ if (!ctx) {
3433
+ return sendForbidden403(
3434
+ res,
3435
+ "Missing or invalid OpenHI JWT claims (tenant, workspace, or audit context)."
3436
+ );
3437
+ }
3438
+ const userId = String(req.params.id);
3439
+ if (userId !== ctx.actorId) {
3440
+ return sendForbidden403(
3441
+ res,
3442
+ "Caller is not authorized to read RoleAssignments for another User."
3443
+ );
3444
+ }
3445
+ const parsed = parseCommonListQuery(req, res, {
3446
+ extraKeys: ["mode", "tenantId"]
3447
+ });
3448
+ if (!parsed.ok) {
3449
+ return parsed.response;
3450
+ }
3451
+ const rawMode = req.query.mode;
3452
+ let mode = "all";
3453
+ if (rawMode !== void 0) {
3454
+ if (typeof rawMode !== "string" || !ALLOWED_MODES2.includes(rawMode)) {
3455
+ return sendInvalidQuery400(
3456
+ res,
3457
+ `Query parameter \`mode\` must be one of: ${ALLOWED_MODES2.join(", ")}.`
3458
+ );
3459
+ }
3460
+ mode = rawMode;
3461
+ }
3462
+ const rawTenantId = req.query.tenantId;
3463
+ let tenantId;
3464
+ if (rawTenantId !== void 0) {
3465
+ if (typeof rawTenantId !== "string" || rawTenantId.length === 0) {
3466
+ return sendInvalidQuery400(
3467
+ res,
3468
+ "Query parameter `tenantId` must be a non-empty string."
3469
+ );
3470
+ }
3471
+ tenantId = rawTenantId;
3472
+ }
3473
+ if (mode === "workspaceInTenant" && !tenantId) {
3474
+ return sendInvalidQuery400(
3475
+ res,
3476
+ 'Query parameter `tenantId` is required when `mode === "workspaceInTenant"`.'
3477
+ );
3478
+ }
3479
+ try {
3480
+ const result = await roleAssignmentListByUserOperation({
3481
+ userId,
3482
+ mode,
3483
+ tenantId,
3484
+ cursor: parsed.value.cursor,
3485
+ ...parsed.value.limit !== void 0 && { limit: parsed.value.limit },
3486
+ ...parsed.value.order !== void 0 && { order: parsed.value.order }
3487
+ });
3488
+ const basePath = `${BASE_PATH.USER}/${userId}/RoleAssignment`;
3489
+ const entries = result.items.map((row) => ({
3490
+ fullUrl: `${BASE_PATH.ROLEASSIGNMENT}/${row.roleAssignmentId}`,
3491
+ resource: buildRoleAssignmentUserProjectionEntryResource(row)
3492
+ }));
3493
+ return res.json({
3494
+ resourceType: "Bundle",
3495
+ type: "searchset",
3496
+ total: entries.length,
3497
+ link: buildPaginationLinks({
3498
+ basePath,
3499
+ query: req.query,
3500
+ nextCursor: result.cursor
3501
+ }),
3502
+ entry: entries
3503
+ });
3504
+ } catch (err) {
3505
+ return sendOperationOutcome500(
3506
+ res,
3507
+ err,
3508
+ "GET /User/:id/RoleAssignment error:"
3509
+ );
3510
+ }
3511
+ }
3512
+
3513
+ // src/data/rest-api/routes/control/user/user-list-route.ts
3514
+ async function listUsersRoute(req, res) {
3515
+ return handleListRoute({
3516
+ req,
3517
+ res,
3518
+ basePath: BASE_PATH.USER,
3519
+ listOperation: listUsersOperation,
3520
+ errorLogContext: "GET /User list error:"
3521
+ });
3522
+ }
3523
+
3524
+ // src/data/rest-api/routes/control/user/user-update-route.ts
3525
+ async function updateUserRoute(req, res) {
3526
+ const bodyResult = requireJsonBody(req, res);
3527
+ if ("errorResponse" in bodyResult) return bodyResult.errorResponse;
3528
+ const id = String(req.params.id);
3529
+ const ctx = req.openhiContext;
3530
+ const body = bodyResult.body;
3531
+ try {
3532
+ const result = await updateUserOperation({
3533
+ context: ctx,
3534
+ id,
3535
+ body: {
3536
+ resource: body?.resource
3537
+ }
3538
+ });
3539
+ return res.json({ ...result.resource, meta: result.meta });
3540
+ } catch (err) {
3541
+ const status = domainErrorToHttpStatus(err);
3542
+ if (status === 404) {
3543
+ return res.status(404).json({
3544
+ resourceType: "OperationOutcome",
3545
+ issue: [
3546
+ {
3547
+ severity: "error",
3548
+ code: "not-found",
3549
+ diagnostics: err instanceof NotFoundError ? err.message : `User ${id} not found`
3550
+ }
3551
+ ]
3552
+ });
3553
+ }
3554
+ console.error("PUT User error:", err);
3555
+ return res.status(500).json({
3556
+ resourceType: "OperationOutcome",
3557
+ issue: [
3558
+ { severity: "error", code: "exception", diagnostics: String(err) }
3559
+ ]
3560
+ });
3561
+ }
3562
+ }
3563
+
3564
+ // src/data/rest-api/routes/control/user/user.ts
3565
+ var router6 = express6.Router();
3566
+ router6.get("/", listUsersRoute);
3567
+ router6.get("/:id", getUserByIdRoute);
3568
+ router6.post("/", createUserRoute);
3569
+ router6.put("/:id", updateUserRoute);
3570
+ router6.delete("/:id", deleteUserRoute);
3571
+ router6.get("/:id/Membership", listUserMembershipsRoute);
3572
+ router6.get("/:id/RoleAssignment", listUserRoleAssignmentsRoute);
3573
+ router6.get("/:id/Configuration", listUserConfigurationsRoute);
2297
3574
 
2298
3575
  // src/data/rest-api/routes/control/user/user-operations.ts
2299
3576
  import express7 from "express";
@@ -2312,15 +3589,22 @@ function getCognitoSubFromRequest(req) {
2312
3589
  }
2313
3590
 
2314
3591
  // src/data/rest-api/routes/control/user/user-current-route.ts
3592
+ var INCLUDE_TOKENS = [
3593
+ "memberships",
3594
+ "roleassignments",
3595
+ "configurations"
3596
+ ];
3597
+ var BOOTSTRAP_PROJECTION_PAGE_CAP = 100;
2315
3598
  async function userCurrentRoute(req, res) {
2316
- if (Object.keys(req.query ?? {}).length > 0) {
3599
+ const includeResult = parseIncludeParam(req.query);
3600
+ if (!includeResult.ok) {
2317
3601
  return res.status(400).json({
2318
3602
  resourceType: "OperationOutcome",
2319
3603
  issue: [
2320
3604
  {
2321
3605
  severity: "error",
2322
3606
  code: "invalid",
2323
- diagnostics: "GET /User/$current does not accept query parameters."
3607
+ diagnostics: includeResult.diagnostics
2324
3608
  }
2325
3609
  ]
2326
3610
  });
@@ -2376,16 +3660,237 @@ async function userCurrentRoute(req, res) {
2376
3660
  });
2377
3661
  }
2378
3662
  const parsedResource = JSON.parse(found.resource);
2379
- res.setHeader("Cache-Control", "no-store");
2380
- return res.json({
3663
+ const userResource = {
2381
3664
  resourceType: "User",
2382
3665
  id: found.id,
2383
3666
  ...parsedResource
3667
+ };
3668
+ res.setHeader("Cache-Control", "no-store");
3669
+ const include = includeResult.include;
3670
+ if (!include) {
3671
+ return res.json(userResource);
3672
+ }
3673
+ const [memberships, roleAssignments, configurations] = await Promise.all([
3674
+ include.memberships ? membershipListByUserOperation({
3675
+ userId: found.id,
3676
+ limit: BOOTSTRAP_PROJECTION_PAGE_CAP
3677
+ }) : Promise.resolve({ items: [], cursor: null }),
3678
+ include.roleAssignments ? roleAssignmentListByUserOperation({
3679
+ userId: found.id,
3680
+ limit: BOOTSTRAP_PROJECTION_PAGE_CAP
3681
+ }) : Promise.resolve({ items: [], cursor: null }),
3682
+ include.configurations ? configurationListByUserOperation({
3683
+ userId: found.id,
3684
+ limit: BOOTSTRAP_PROJECTION_PAGE_CAP
3685
+ }) : Promise.resolve({ items: [], cursor: null })
3686
+ ]);
3687
+ const basePath = `${BASE_PATH.USER}/$current`;
3688
+ const entries = [];
3689
+ entries.push({
3690
+ fullUrl: `${BASE_PATH.USER}/${found.id}`,
3691
+ resource: userResource
3692
+ });
3693
+ for (const row of memberships.items) {
3694
+ entries.push({
3695
+ fullUrl: `${BASE_PATH.MEMBERSHIP}/${row.membershipId}`,
3696
+ resource: buildMembershipEntryResource(row)
3697
+ });
3698
+ }
3699
+ for (const row of roleAssignments.items) {
3700
+ entries.push({
3701
+ fullUrl: `${BASE_PATH.ROLEASSIGNMENT}/${row.roleAssignmentId}`,
3702
+ resource: buildRoleAssignmentEntryResource(row)
3703
+ });
3704
+ }
3705
+ for (const row of configurations.items) {
3706
+ entries.push({
3707
+ fullUrl: `${BASE_PATH.CONFIGURATION}/${row.configurationId}`,
3708
+ resource: buildConfigurationEntryResource(row)
3709
+ });
3710
+ }
3711
+ return res.json({
3712
+ resourceType: "Bundle",
3713
+ type: "searchset",
3714
+ total: entries.length,
3715
+ link: [{ relation: "self", url: basePath }],
3716
+ entry: entries
2384
3717
  });
2385
3718
  } catch (err) {
2386
3719
  return sendOperationOutcome500(res, err, "GET /User/$current error:");
2387
3720
  }
2388
3721
  }
3722
+ function parseIncludeParam(query) {
3723
+ const q = query ?? {};
3724
+ const keys = Object.keys(q);
3725
+ const extra = keys.filter((k) => k !== "include");
3726
+ if (extra.length > 0) {
3727
+ return {
3728
+ ok: false,
3729
+ diagnostics: "GET /User/$current only accepts the optional `?include=` query parameter."
3730
+ };
3731
+ }
3732
+ if (keys.length === 0) {
3733
+ return { ok: true, include: void 0 };
3734
+ }
3735
+ const raw = q.include;
3736
+ if (typeof raw !== "string") {
3737
+ return {
3738
+ ok: false,
3739
+ diagnostics: "GET /User/$current: `include` query parameter must be a comma-separated string."
3740
+ };
3741
+ }
3742
+ const trimmed = raw.trim();
3743
+ if (trimmed.length === 0) {
3744
+ return {
3745
+ ok: false,
3746
+ diagnostics: "GET /User/$current: `include` query parameter must not be empty."
3747
+ };
3748
+ }
3749
+ const tokens = trimmed.split(",").map((t) => t.trim().toLowerCase()).filter((t) => t.length > 0);
3750
+ const include = {
3751
+ memberships: false,
3752
+ roleAssignments: false,
3753
+ configurations: false
3754
+ };
3755
+ const allowed = INCLUDE_TOKENS;
3756
+ const mutable = {
3757
+ ...include
3758
+ };
3759
+ for (const token of tokens) {
3760
+ if (!allowed.includes(token)) {
3761
+ return {
3762
+ ok: false,
3763
+ diagnostics: `GET /User/$current: unknown \`include\` token \`${token}\`. Allowed: ${INCLUDE_TOKENS.join(", ")}.`
3764
+ };
3765
+ }
3766
+ const t = token;
3767
+ if (t === "memberships") {
3768
+ mutable.memberships = true;
3769
+ } else if (t === "roleassignments") {
3770
+ mutable.roleAssignments = true;
3771
+ } else if (t === "configurations") {
3772
+ mutable.configurations = true;
3773
+ }
3774
+ }
3775
+ return { ok: true, include: mutable };
3776
+ }
3777
+ function parseSummary(summary) {
3778
+ if (typeof summary !== "string" || summary.length === 0) {
3779
+ return {};
3780
+ }
3781
+ try {
3782
+ const parsed = JSON.parse(summary);
3783
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3784
+ return parsed;
3785
+ }
3786
+ } catch {
3787
+ }
3788
+ return {};
3789
+ }
3790
+ function buildMembershipEntryResource(row) {
3791
+ return {
3792
+ resourceType: "Membership",
3793
+ id: row.membershipId,
3794
+ ...parseSummary(row.summary),
3795
+ meta: { versionId: row.vid, lastUpdated: row.lastUpdated },
3796
+ extension: [
3797
+ {
3798
+ url: "https://openhi.org/fhir/StructureDefinition/projection-tenant-id",
3799
+ valueString: row.tenantId
3800
+ },
3801
+ ...row.workspaceId ? [
3802
+ {
3803
+ url: "https://openhi.org/fhir/StructureDefinition/projection-workspace-id",
3804
+ valueString: row.workspaceId
3805
+ }
3806
+ ] : [],
3807
+ ...row.denormalizedTenantName ? [
3808
+ {
3809
+ url: "https://openhi.org/fhir/StructureDefinition/projection-denormalized-tenant-name",
3810
+ valueString: row.denormalizedTenantName
3811
+ }
3812
+ ] : [],
3813
+ ...row.denormalizedUserName ? [
3814
+ {
3815
+ url: "https://openhi.org/fhir/StructureDefinition/projection-denormalized-user-name",
3816
+ valueString: row.denormalizedUserName
3817
+ }
3818
+ ] : [],
3819
+ ...row.denormalizedWorkspaceName ? [
3820
+ {
3821
+ url: "https://openhi.org/fhir/StructureDefinition/projection-denormalized-workspace-name",
3822
+ valueString: row.denormalizedWorkspaceName
3823
+ }
3824
+ ] : []
3825
+ ]
3826
+ };
3827
+ }
3828
+ function buildRoleAssignmentEntryResource(row) {
3829
+ return {
3830
+ resourceType: "RoleAssignment",
3831
+ id: row.roleAssignmentId,
3832
+ ...parseSummary(row.summary),
3833
+ meta: { versionId: row.vid, lastUpdated: row.lastUpdated },
3834
+ extension: [
3835
+ {
3836
+ url: "https://openhi.org/fhir/StructureDefinition/projection-tenant-id",
3837
+ valueString: row.tenantId
3838
+ },
3839
+ ...row.workspaceId ? [
3840
+ {
3841
+ url: "https://openhi.org/fhir/StructureDefinition/projection-workspace-id",
3842
+ valueString: row.workspaceId
3843
+ }
3844
+ ] : [],
3845
+ {
3846
+ url: "https://openhi.org/fhir/StructureDefinition/projection-role-id",
3847
+ valueString: row.roleId
3848
+ },
3849
+ ...row.denormalizedTenantName ? [
3850
+ {
3851
+ url: "https://openhi.org/fhir/StructureDefinition/projection-denormalized-tenant-name",
3852
+ valueString: row.denormalizedTenantName
3853
+ }
3854
+ ] : [],
3855
+ ...row.denormalizedUserName ? [
3856
+ {
3857
+ url: "https://openhi.org/fhir/StructureDefinition/projection-denormalized-user-name",
3858
+ valueString: row.denormalizedUserName
3859
+ }
3860
+ ] : [],
3861
+ ...row.denormalizedRoleName ? [
3862
+ {
3863
+ url: "https://openhi.org/fhir/StructureDefinition/projection-denormalized-role-name",
3864
+ valueString: row.denormalizedRoleName
3865
+ }
3866
+ ] : []
3867
+ ]
3868
+ };
3869
+ }
3870
+ function buildConfigurationEntryResource(row) {
3871
+ return {
3872
+ resourceType: "Configuration",
3873
+ id: row.configurationId,
3874
+ ...parseSummary(row.summary),
3875
+ meta: { versionId: row.vid, lastUpdated: row.lastUpdated },
3876
+ extension: [
3877
+ {
3878
+ url: "https://openhi.org/fhir/StructureDefinition/projection-tenant-id",
3879
+ valueString: row.tenantId
3880
+ },
3881
+ {
3882
+ url: "https://openhi.org/fhir/StructureDefinition/projection-scope",
3883
+ valueString: row.scope
3884
+ },
3885
+ ...row.displayName ? [
3886
+ {
3887
+ url: "https://openhi.org/fhir/StructureDefinition/projection-display-name",
3888
+ valueString: row.displayName
3889
+ }
3890
+ ] : []
3891
+ ]
3892
+ };
3893
+ }
2389
3894
  function hasNonEmptyBody(body) {
2390
3895
  if (body == null) {
2391
3896
  return false;
@@ -2600,6 +4105,204 @@ async function getWorkspaceByIdRoute(req, res) {
2600
4105
  }
2601
4106
  }
2602
4107
 
4108
+ // src/data/rest-api/routes/control/workspace/workspace-list-configurations-route.ts
4109
+ async function listWorkspaceConfigurationsRoute(req, res) {
4110
+ const ctx = req.openhiContext;
4111
+ if (!ctx) {
4112
+ return sendForbidden403(
4113
+ res,
4114
+ "Missing or invalid OpenHI JWT claims (tenant, workspace, or audit context)."
4115
+ );
4116
+ }
4117
+ const workspaceId = String(req.params.id);
4118
+ const tenantId = ctx.tenantId;
4119
+ const parsed = parseCommonListQuery(req, res);
4120
+ if (!parsed.ok) {
4121
+ return parsed.response;
4122
+ }
4123
+ try {
4124
+ const memberCheck = await membershipListByUserOperation({
4125
+ userId: ctx.actorId,
4126
+ mode: "workspaceInTenant",
4127
+ tenantId
4128
+ });
4129
+ const hasMembership = memberCheck.items.some(
4130
+ (row) => row.workspaceId === workspaceId
4131
+ );
4132
+ if (!hasMembership) {
4133
+ return sendForbidden403(
4134
+ res,
4135
+ `Caller is not a member of Workspace/${workspaceId} in Tenant/${tenantId}.`
4136
+ );
4137
+ }
4138
+ const result = await configurationListByWorkspaceOperation({
4139
+ tenantId,
4140
+ workspaceId,
4141
+ cursor: parsed.value.cursor,
4142
+ ...parsed.value.limit !== void 0 && { limit: parsed.value.limit },
4143
+ ...parsed.value.order !== void 0 && { order: parsed.value.order }
4144
+ });
4145
+ const basePath = `${BASE_PATH.WORKSPACE}/${workspaceId}/Configuration`;
4146
+ const entries = result.items.map((row) => ({
4147
+ fullUrl: `${BASE_PATH.CONFIGURATION}/${row.configurationId}`,
4148
+ resource: buildConfigurationWorkspaceProjectionEntryResource(row)
4149
+ }));
4150
+ return res.json({
4151
+ resourceType: "Bundle",
4152
+ type: "searchset",
4153
+ total: entries.length,
4154
+ link: buildPaginationLinks({
4155
+ basePath,
4156
+ query: req.query,
4157
+ nextCursor: result.cursor
4158
+ }),
4159
+ entry: entries
4160
+ });
4161
+ } catch (err) {
4162
+ return sendOperationOutcome500(
4163
+ res,
4164
+ err,
4165
+ "GET /Workspace/:id/Configuration error:"
4166
+ );
4167
+ }
4168
+ }
4169
+
4170
+ // src/data/rest-api/routes/control/workspace/workspace-list-memberships-route.ts
4171
+ async function listWorkspaceMembershipsRoute(req, res) {
4172
+ const ctx = req.openhiContext;
4173
+ if (!ctx) {
4174
+ return sendForbidden403(
4175
+ res,
4176
+ "Missing or invalid OpenHI JWT claims (tenant, workspace, or audit context)."
4177
+ );
4178
+ }
4179
+ const workspaceId = String(req.params.id);
4180
+ const tenantId = ctx.tenantId;
4181
+ const parsed = parseCommonListQuery(req, res);
4182
+ if (!parsed.ok) {
4183
+ return parsed.response;
4184
+ }
4185
+ try {
4186
+ const memberCheck = await membershipListByUserOperation({
4187
+ userId: ctx.actorId,
4188
+ mode: "workspaceInTenant",
4189
+ tenantId
4190
+ });
4191
+ const hasMembership = memberCheck.items.some(
4192
+ (row) => row.workspaceId === workspaceId
4193
+ );
4194
+ if (!hasMembership) {
4195
+ return sendForbidden403(
4196
+ res,
4197
+ `Caller is not a member of Workspace/${workspaceId} in Tenant/${tenantId}.`
4198
+ );
4199
+ }
4200
+ const result = await membershipListByWorkspaceOperation({
4201
+ tenantId,
4202
+ workspaceId,
4203
+ cursor: parsed.value.cursor,
4204
+ ...parsed.value.limit !== void 0 && { limit: parsed.value.limit },
4205
+ ...parsed.value.order !== void 0 && { order: parsed.value.order }
4206
+ });
4207
+ const basePath = `${BASE_PATH.WORKSPACE}/${workspaceId}/Membership`;
4208
+ const entries = result.items.map((row) => ({
4209
+ fullUrl: `${BASE_PATH.MEMBERSHIP}/${row.membershipId}`,
4210
+ resource: buildMembershipWorkspaceProjectionEntryResource(row)
4211
+ }));
4212
+ return res.json({
4213
+ resourceType: "Bundle",
4214
+ type: "searchset",
4215
+ total: entries.length,
4216
+ link: buildPaginationLinks({
4217
+ basePath,
4218
+ query: req.query,
4219
+ nextCursor: result.cursor
4220
+ }),
4221
+ entry: entries
4222
+ });
4223
+ } catch (err) {
4224
+ return sendOperationOutcome500(
4225
+ res,
4226
+ err,
4227
+ "GET /Workspace/:id/Membership error:"
4228
+ );
4229
+ }
4230
+ }
4231
+
4232
+ // src/data/rest-api/routes/control/workspace/workspace-list-role-assignments-route.ts
4233
+ async function listWorkspaceRoleAssignmentsRoute(req, res) {
4234
+ const ctx = req.openhiContext;
4235
+ if (!ctx) {
4236
+ return sendForbidden403(
4237
+ res,
4238
+ "Missing or invalid OpenHI JWT claims (tenant, workspace, or audit context)."
4239
+ );
4240
+ }
4241
+ const workspaceId = String(req.params.id);
4242
+ const tenantId = ctx.tenantId;
4243
+ const parsed = parseCommonListQuery(req, res, { extraKeys: ["roleId"] });
4244
+ if (!parsed.ok) {
4245
+ return parsed.response;
4246
+ }
4247
+ const rawRoleId = req.query.roleId;
4248
+ let roleId;
4249
+ if (rawRoleId !== void 0) {
4250
+ if (typeof rawRoleId !== "string" || rawRoleId.length === 0) {
4251
+ return sendInvalidQuery400(
4252
+ res,
4253
+ "Query parameter `roleId` must be a non-empty string."
4254
+ );
4255
+ }
4256
+ roleId = rawRoleId;
4257
+ }
4258
+ try {
4259
+ const memberCheck = await membershipListByUserOperation({
4260
+ userId: ctx.actorId,
4261
+ mode: "workspaceInTenant",
4262
+ tenantId
4263
+ });
4264
+ const hasMembership = memberCheck.items.some(
4265
+ (row) => row.workspaceId === workspaceId
4266
+ );
4267
+ if (!hasMembership) {
4268
+ return sendForbidden403(
4269
+ res,
4270
+ `Caller is not a member of Workspace/${workspaceId} in Tenant/${tenantId}.`
4271
+ );
4272
+ }
4273
+ const result = await roleAssignmentListByWorkspaceOperation({
4274
+ tenantId,
4275
+ workspaceId,
4276
+ roleId,
4277
+ cursor: parsed.value.cursor,
4278
+ ...parsed.value.limit !== void 0 && { limit: parsed.value.limit },
4279
+ ...parsed.value.order !== void 0 && { order: parsed.value.order }
4280
+ });
4281
+ const basePath = `${BASE_PATH.WORKSPACE}/${workspaceId}/RoleAssignment`;
4282
+ const entries = result.items.map((row) => ({
4283
+ fullUrl: `${BASE_PATH.ROLEASSIGNMENT}/${row.roleAssignmentId}`,
4284
+ resource: buildRoleAssignmentWorkspaceProjectionEntryResource(row)
4285
+ }));
4286
+ return res.json({
4287
+ resourceType: "Bundle",
4288
+ type: "searchset",
4289
+ total: entries.length,
4290
+ link: buildPaginationLinks({
4291
+ basePath,
4292
+ query: req.query,
4293
+ nextCursor: result.cursor
4294
+ }),
4295
+ entry: entries
4296
+ });
4297
+ } catch (err) {
4298
+ return sendOperationOutcome500(
4299
+ res,
4300
+ err,
4301
+ "GET /Workspace/:id/RoleAssignment error:"
4302
+ );
4303
+ }
4304
+ }
4305
+
2603
4306
  // src/data/operations/control/workspace/workspace-list-operation.ts
2604
4307
  var SK8 = "CURRENT";
2605
4308
  async function listWorkspacesOperation(params) {
@@ -2720,6 +4423,9 @@ router8.get("/:id", getWorkspaceByIdRoute);
2720
4423
  router8.post("/", createWorkspaceRoute);
2721
4424
  router8.put("/:id", updateWorkspaceRoute);
2722
4425
  router8.delete("/:id", deleteWorkspaceRoute);
4426
+ router8.get("/:id/Membership", listWorkspaceMembershipsRoute);
4427
+ router8.get("/:id/RoleAssignment", listWorkspaceRoleAssignmentsRoute);
4428
+ router8.get("/:id/Configuration", listWorkspaceConfigurationsRoute);
2723
4429
 
2724
4430
  // src/data/rest-api/routes/data/account/account.ts
2725
4431
  import express9 from "express";