@openhi/constructs 0.0.110 → 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 (117) hide show
  1. package/lib/chunk-23PUSHBV.mjs +24 -0
  2. package/lib/chunk-23PUSHBV.mjs.map +1 -0
  3. package/lib/chunk-2O3CXY2C.mjs +79 -0
  4. package/lib/chunk-2O3CXY2C.mjs.map +1 -0
  5. package/lib/{chunk-7FUAMZOF.mjs → chunk-53OHXLIL.mjs} +3 -3
  6. package/lib/chunk-6NBGYGFL.mjs +1803 -0
  7. package/lib/chunk-6NBGYGFL.mjs.map +1 -0
  8. package/lib/chunk-7RZHFI77.mjs +22 -0
  9. package/lib/chunk-7RZHFI77.mjs.map +1 -0
  10. package/lib/{chunk-7Q2IJ2J5.mjs → chunk-CUUKXDB2.mjs} +6 -6
  11. package/lib/chunk-FYHBHHWK.mjs +47 -0
  12. package/lib/chunk-FYHBHHWK.mjs.map +1 -0
  13. package/lib/{chunk-MULKGFIJ.mjs → chunk-GBDIGTNV.mjs} +165 -10
  14. package/lib/chunk-GBDIGTNV.mjs.map +1 -0
  15. package/lib/chunk-HQ67J7BP.mjs +199 -0
  16. package/lib/chunk-HQ67J7BP.mjs.map +1 -0
  17. package/lib/{chunk-AJ3G3THO.mjs → chunk-KO64HPWQ.mjs} +2 -2
  18. package/lib/{chunk-BB5MK4L3.mjs → chunk-KSFC72TT.mjs} +3 -3
  19. package/lib/{chunk-2TPJ6HOF.mjs → chunk-NZRW7ROK.mjs} +72 -54
  20. package/lib/chunk-NZRW7ROK.mjs.map +1 -0
  21. package/lib/chunk-QJDHVMKT.mjs +117 -0
  22. package/lib/chunk-QJDHVMKT.mjs.map +1 -0
  23. package/lib/{chunk-IS4VQRI4.mjs → chunk-QMBJ4VHC.mjs} +12 -47
  24. package/lib/chunk-QMBJ4VHC.mjs.map +1 -0
  25. package/lib/chunk-TRY7JGWO.mjs +16 -0
  26. package/lib/chunk-TRY7JGWO.mjs.map +1 -0
  27. package/lib/chunk-W4KR4CSL.mjs +236 -0
  28. package/lib/chunk-W4KR4CSL.mjs.map +1 -0
  29. package/lib/{chunk-AGF3RAAZ.mjs → chunk-WPCBVDFZ.mjs} +2 -2
  30. package/lib/chunk-WQWFVEVX.mjs +66 -0
  31. package/lib/chunk-WQWFVEVX.mjs.map +1 -0
  32. package/lib/{chunk-SYBADQXI.mjs → chunk-ZM4GDHHC.mjs} +77 -2
  33. package/lib/chunk-ZM4GDHHC.mjs.map +1 -0
  34. package/lib/data-store-postgres-replication.handler.js +26 -17
  35. package/lib/data-store-postgres-replication.handler.js.map +1 -1
  36. package/lib/data-store-postgres-replication.handler.mjs +5 -65
  37. package/lib/data-store-postgres-replication.handler.mjs.map +1 -1
  38. package/lib/delete-chunk.handler.d.mts +29 -0
  39. package/lib/delete-chunk.handler.d.ts +29 -0
  40. package/lib/delete-chunk.handler.js +2716 -0
  41. package/lib/delete-chunk.handler.js.map +1 -0
  42. package/lib/delete-chunk.handler.mjs +47 -0
  43. package/lib/delete-chunk.handler.mjs.map +1 -0
  44. package/lib/events-CjS-sm0W.d.mts +107 -0
  45. package/lib/events-CjS-sm0W.d.ts +107 -0
  46. package/lib/events-Da_cFgtc.d.mts +208 -0
  47. package/lib/events-Da_cFgtc.d.ts +208 -0
  48. package/lib/finalize.handler.d.mts +35 -0
  49. package/lib/finalize.handler.d.ts +35 -0
  50. package/lib/finalize.handler.js +875 -0
  51. package/lib/finalize.handler.js.map +1 -0
  52. package/lib/finalize.handler.mjs +166 -0
  53. package/lib/finalize.handler.mjs.map +1 -0
  54. package/lib/index.d.mts +189 -2
  55. package/lib/index.d.ts +500 -3
  56. package/lib/index.js +1753 -174
  57. package/lib/index.js.map +1 -1
  58. package/lib/index.mjs +571 -17
  59. package/lib/index.mjs.map +1 -1
  60. package/lib/list-chunks.handler.d.mts +28 -0
  61. package/lib/list-chunks.handler.d.ts +28 -0
  62. package/lib/list-chunks.handler.js +2746 -0
  63. package/lib/list-chunks.handler.js.map +1 -0
  64. package/lib/list-chunks.handler.mjs +54 -0
  65. package/lib/list-chunks.handler.mjs.map +1 -0
  66. package/lib/platform-deploy-bridge.handler.js +76 -1
  67. package/lib/platform-deploy-bridge.handler.js.map +1 -1
  68. package/lib/platform-deploy-bridge.handler.mjs +1 -1
  69. package/lib/pre-token-generation.handler.js +1106 -155
  70. package/lib/pre-token-generation.handler.js.map +1 -1
  71. package/lib/pre-token-generation.handler.mjs +6 -4
  72. package/lib/pre-token-generation.handler.mjs.map +1 -1
  73. package/lib/provision-default-workspace.handler.js +1529 -142
  74. package/lib/provision-default-workspace.handler.js.map +1 -1
  75. package/lib/provision-default-workspace.handler.mjs +8 -4
  76. package/lib/provision-default-workspace.handler.mjs.map +1 -1
  77. package/lib/rename-finalize.handler.d.mts +30 -0
  78. package/lib/rename-finalize.handler.d.ts +30 -0
  79. package/lib/rename-finalize.handler.js +795 -0
  80. package/lib/rename-finalize.handler.js.map +1 -0
  81. package/lib/rename-finalize.handler.mjs +90 -0
  82. package/lib/rename-finalize.handler.mjs.map +1 -0
  83. package/lib/rename-list-targets.handler.d.mts +26 -0
  84. package/lib/rename-list-targets.handler.d.ts +26 -0
  85. package/lib/rename-list-targets.handler.js +2985 -0
  86. package/lib/rename-list-targets.handler.js.map +1 -0
  87. package/lib/rename-list-targets.handler.mjs +431 -0
  88. package/lib/rename-list-targets.handler.mjs.map +1 -0
  89. package/lib/rename-rewrite-chunk.handler.d.mts +35 -0
  90. package/lib/rename-rewrite-chunk.handler.d.ts +35 -0
  91. package/lib/rename-rewrite-chunk.handler.js +2021 -0
  92. package/lib/rename-rewrite-chunk.handler.js.map +1 -0
  93. package/lib/rename-rewrite-chunk.handler.mjs +27 -0
  94. package/lib/rename-rewrite-chunk.handler.mjs.map +1 -0
  95. package/lib/rest-api-lambda.handler.js +4087 -921
  96. package/lib/rest-api-lambda.handler.js.map +1 -1
  97. package/lib/rest-api-lambda.handler.mjs +1827 -81
  98. package/lib/rest-api-lambda.handler.mjs.map +1 -1
  99. package/lib/seed-demo-data.handler.js +1588 -124
  100. package/lib/seed-demo-data.handler.js.map +1 -1
  101. package/lib/seed-demo-data.handler.mjs +10 -6
  102. package/lib/seed-system-data.handler.js +1179 -155
  103. package/lib/seed-system-data.handler.js.map +1 -1
  104. package/lib/seed-system-data.handler.mjs +5 -4
  105. package/lib/seed-system-data.handler.mjs.map +1 -1
  106. package/package.json +1 -1
  107. package/lib/chunk-2TPJ6HOF.mjs.map +0 -1
  108. package/lib/chunk-IS4VQRI4.mjs.map +0 -1
  109. package/lib/chunk-MULKGFIJ.mjs.map +0 -1
  110. package/lib/chunk-QR5JVSCF.mjs +0 -862
  111. package/lib/chunk-QR5JVSCF.mjs.map +0 -1
  112. package/lib/chunk-SYBADQXI.mjs.map +0 -1
  113. /package/lib/{chunk-7FUAMZOF.mjs.map → chunk-53OHXLIL.mjs.map} +0 -0
  114. /package/lib/{chunk-7Q2IJ2J5.mjs.map → chunk-CUUKXDB2.mjs.map} +0 -0
  115. /package/lib/{chunk-AJ3G3THO.mjs.map → chunk-KO64HPWQ.mjs.map} +0 -0
  116. /package/lib/{chunk-BB5MK4L3.mjs.map → chunk-KSFC72TT.mjs.map} +0 -0
  117. /package/lib/{chunk-AGF3RAAZ.mjs.map → chunk-WPCBVDFZ.mjs.map} +0 -0
@@ -1,33 +1,46 @@
1
+ import {
2
+ buildSchemaBootstrapStatements
3
+ } from "./chunk-2O3CXY2C.mjs";
1
4
  import {
2
5
  createRoleOperation
3
- } from "./chunk-AJ3G3THO.mjs";
6
+ } from "./chunk-KO64HPWQ.mjs";
4
7
  import {
5
8
  getRoleByIdOperation
6
- } from "./chunk-7FUAMZOF.mjs";
9
+ } from "./chunk-53OHXLIL.mjs";
7
10
  import {
8
11
  listMembershipsOperation,
9
12
  listPractitionerRolesOperation,
10
13
  listRoleAssignmentsOperation
11
- } from "./chunk-BB5MK4L3.mjs";
14
+ } from "./chunk-KSFC72TT.mjs";
12
15
  import {
13
16
  createMembershipOperation,
14
17
  createRoleAssignmentOperation,
15
18
  createTenantOperation,
16
- createWorkspaceOperation
17
- } 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";
18
33
  import {
19
34
  createUserOperation,
20
35
  deleteUserOperation,
21
36
  findUserBySubOperation,
22
37
  getUserByIdOperation,
23
38
  listUsersOperation,
39
+ membershipListByUserOperation,
24
40
  switchUserTenantWorkspaceOperation,
25
41
  updateUserOperation
26
- } from "./chunk-2TPJ6HOF.mjs";
42
+ } from "./chunk-NZRW7ROK.mjs";
27
43
  import {
28
- ForbiddenError,
29
- NotFoundError,
30
- ValidationError,
31
44
  batchGetWithRetry,
32
45
  buildUpdatedResourceWithAudit,
33
46
  compressResource,
@@ -35,17 +48,23 @@ import {
35
48
  decompressResource,
36
49
  deleteDataEntityById,
37
50
  dispatchListMode,
38
- domainErrorToHttpStatus,
39
51
  getDataEntityById,
40
52
  getDynamoDataService,
41
53
  listDataEntitiesByWorkspace,
42
54
  mergeAuditIntoMeta,
43
55
  updateDataEntityById
44
- } from "./chunk-IS4VQRI4.mjs";
56
+ } from "./chunk-QMBJ4VHC.mjs";
57
+ import {
58
+ ForbiddenError,
59
+ NotFoundError,
60
+ ValidationError,
61
+ domainErrorToHttpStatus
62
+ } from "./chunk-FYHBHHWK.mjs";
45
63
  import {
46
64
  SHARD_COUNT,
47
65
  getDynamoControlService
48
- } from "./chunk-QR5JVSCF.mjs";
66
+ } from "./chunk-6NBGYGFL.mjs";
67
+ import "./chunk-TRY7JGWO.mjs";
49
68
  import "./chunk-LZOMFHX3.mjs";
50
69
 
51
70
  // src/data/lambda/rest-api-lambda.handler.ts
@@ -116,6 +135,81 @@ function openHiContextMiddleware(req, res, next) {
116
135
  // src/data/rest-api/routes/control/configuration/configuration.ts
117
136
  import express from "express";
118
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
+
119
213
  // src/data/operations/control/configuration/configuration-create-operation.ts
120
214
  var SK = "CURRENT";
121
215
  async function createConfigurationOperation(params) {
@@ -144,7 +238,28 @@ async function createConfigurationOperation(params) {
144
238
  roleId
145
239
  });
146
240
  const service = getDynamoControlService(tableName);
147
- 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 = {
148
263
  tenantId,
149
264
  workspaceId,
150
265
  userId,
@@ -156,7 +271,25 @@ async function createConfigurationOperation(params) {
156
271
  vid,
157
272
  lastUpdated,
158
273
  sk: SK
159
- }).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 });
160
293
  const resource = typeof resourcePayload === "object" ? resourcePayload : JSON.parse(resourceStr);
161
294
  return {
162
295
  id,
@@ -461,7 +594,7 @@ async function deleteConfigurationOperation(params) {
461
594
  const { tenantId, workspaceId, actorId, roleId: ctxRoleId } = context;
462
595
  const roleId = ctxRoleId ?? "-";
463
596
  const service = getDynamoControlService(tableName);
464
- await service.entities.configuration.delete({
597
+ const existing = await service.entities.configuration.get({
465
598
  tenantId,
466
599
  workspaceId,
467
600
  userId: actorId,
@@ -469,6 +602,81 @@ async function deleteConfigurationOperation(params) {
469
602
  key: id,
470
603
  sk: SK2
471
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 });
472
680
  }
473
681
 
474
682
  // src/data/rest-api/routes/control/configuration/configuration-delete-route.ts
@@ -947,6 +1155,47 @@ async function updateConfigurationOperation(params) {
947
1155
  lastUpdated,
948
1156
  vid: nextVid
949
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
+ }
950
1199
  const parsedResource = typeof resourcePayload === "object" ? resourcePayload : JSON.parse(resourceStr);
951
1200
  return {
952
1201
  id: existing.data.id,
@@ -1381,7 +1630,78 @@ async function createMembershipRoute(req, res) {
1381
1630
  async function deleteMembershipOperation(params) {
1382
1631
  const { context, id, tableName } = params;
1383
1632
  const service = getDynamoControlService(tableName);
1384
- 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 });
1385
1705
  }
1386
1706
 
1387
1707
  // src/data/rest-api/routes/control/membership/membership-delete-route.ts
@@ -1473,16 +1793,76 @@ async function updateMembershipOperation(params) {
1473
1793
  }
1474
1794
  throw e;
1475
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
+ );
1476
1809
  const summary = JSON.stringify(extractSummary(resource));
1477
- 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 = {
1478
1838
  tenantId: context.tenantId,
1479
1839
  id,
1480
1840
  resource: JSON.stringify(resource),
1481
1841
  summary,
1482
1842
  vid,
1483
1843
  lastUpdated,
1484
- linkedDataIdentityRef
1485
- }).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 });
1486
1866
  return {
1487
1867
  id,
1488
1868
  resource,
@@ -1778,7 +2158,79 @@ async function createRoleAssignmentRoute(req, res) {
1778
2158
  async function deleteRoleAssignmentOperation(params) {
1779
2159
  const { context, id, tableName } = params;
1780
2160
  const service = getDynamoControlService(tableName);
1781
- 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 });
1782
2234
  }
1783
2235
 
1784
2236
  // src/data/rest-api/routes/control/roleassignment/roleassignment-delete-route.ts
@@ -1870,15 +2322,80 @@ async function updateRoleAssignmentOperation(params) {
1870
2322
  const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
1871
2323
  const vid = `${Date.now()}`;
1872
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
+ );
1873
2338
  const summary = JSON.stringify(extractSummary3(resource));
1874
- 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 = {
1875
2371
  tenantId: context.tenantId,
1876
2372
  id,
1877
2373
  resource: JSON.stringify(resource),
1878
2374
  summary,
1879
2375
  vid,
1880
- lastUpdated
1881
- }).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 });
1882
2399
  return {
1883
2400
  id,
1884
2401
  resource,
@@ -2233,67 +2750,830 @@ async function getUserByIdRoute(req, res) {
2233
2750
  }
2234
2751
  }
2235
2752
 
2236
- // src/data/rest-api/routes/control/user/user-list-route.ts
2237
- async function listUsersRoute(req, res) {
2238
- return handleListRoute({
2239
- req,
2240
- res,
2241
- basePath: BASE_PATH.USER,
2242
- listOperation: listUsersOperation,
2243
- errorLogContext: "GET /User list error:"
2244
- });
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 };
2245
2779
  }
2246
2780
 
2247
- // src/data/rest-api/routes/control/user/user-update-route.ts
2248
- async function updateUserRoute(req, res) {
2249
- const bodyResult = requireJsonBody(req, res);
2250
- if ("errorResponse" in bodyResult) return bodyResult.errorResponse;
2251
- const id = String(req.params.id);
2252
- const ctx = req.openhiContext;
2253
- const body = bodyResult.body;
2254
- try {
2255
- const result = await updateUserOperation({
2256
- context: ctx,
2257
- id,
2258
- body: {
2259
- resource: body?.resource
2260
- }
2261
- });
2262
- return res.json({ ...result.resource, meta: result.meta });
2263
- } catch (err) {
2264
- const status = domainErrorToHttpStatus(err);
2265
- if (status === 404) {
2266
- return res.status(404).json({
2267
- resourceType: "OperationOutcome",
2268
- issue: [
2269
- {
2270
- severity: "error",
2271
- code: "not-found",
2272
- diagnostics: err instanceof NotFoundError ? err.message : `User ${id} not found`
2273
- }
2274
- ]
2275
- });
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
+ };
2276
2857
  }
2277
- console.error("PUT User error:", err);
2278
- return res.status(500).json({
2279
- resourceType: "OperationOutcome",
2280
- issue: [
2281
- { severity: "error", code: "exception", diagnostics: String(err) }
2282
- ]
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)
2283
2911
  });
2284
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}`;
2285
2933
  }
2286
2934
 
2287
- // src/data/rest-api/routes/control/user/user.ts
2288
- var router6 = express6.Router();
2289
- router6.get("/", listUsersRoute);
2290
- router6.get("/:id", getUserByIdRoute);
2291
- router6.post("/", createUserRoute);
2292
- router6.put("/:id", updateUserRoute);
2293
- router6.delete("/:id", deleteUserRoute);
2294
-
2295
- // src/data/rest-api/routes/control/user/user-operations.ts
2296
- import express7 from "express";
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);
3574
+
3575
+ // src/data/rest-api/routes/control/user/user-operations.ts
3576
+ import express7 from "express";
2297
3577
 
2298
3578
  // src/data/rest-api/routes/control/user/user-operation-helpers.ts
2299
3579
  import { getCurrentInvoke as getCurrentInvoke2 } from "@codegenie/serverless-express";
@@ -2309,15 +3589,22 @@ function getCognitoSubFromRequest(req) {
2309
3589
  }
2310
3590
 
2311
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;
2312
3598
  async function userCurrentRoute(req, res) {
2313
- if (Object.keys(req.query ?? {}).length > 0) {
3599
+ const includeResult = parseIncludeParam(req.query);
3600
+ if (!includeResult.ok) {
2314
3601
  return res.status(400).json({
2315
3602
  resourceType: "OperationOutcome",
2316
3603
  issue: [
2317
3604
  {
2318
3605
  severity: "error",
2319
3606
  code: "invalid",
2320
- diagnostics: "GET /User/$current does not accept query parameters."
3607
+ diagnostics: includeResult.diagnostics
2321
3608
  }
2322
3609
  ]
2323
3610
  });
@@ -2373,16 +3660,237 @@ async function userCurrentRoute(req, res) {
2373
3660
  });
2374
3661
  }
2375
3662
  const parsedResource = JSON.parse(found.resource);
2376
- res.setHeader("Cache-Control", "no-store");
2377
- return res.json({
3663
+ const userResource = {
2378
3664
  resourceType: "User",
2379
3665
  id: found.id,
2380
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
2381
3717
  });
2382
3718
  } catch (err) {
2383
3719
  return sendOperationOutcome500(res, err, "GET /User/$current error:");
2384
3720
  }
2385
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
+ }
2386
3894
  function hasNonEmptyBody(body) {
2387
3895
  if (body == null) {
2388
3896
  return false;
@@ -2597,6 +4105,204 @@ async function getWorkspaceByIdRoute(req, res) {
2597
4105
  }
2598
4106
  }
2599
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
+
2600
4306
  // src/data/operations/control/workspace/workspace-list-operation.ts
2601
4307
  var SK8 = "CURRENT";
2602
4308
  async function listWorkspacesOperation(params) {
@@ -2717,6 +4423,9 @@ router8.get("/:id", getWorkspaceByIdRoute);
2717
4423
  router8.post("/", createWorkspaceRoute);
2718
4424
  router8.put("/:id", updateWorkspaceRoute);
2719
4425
  router8.delete("/:id", deleteWorkspaceRoute);
4426
+ router8.get("/:id/Membership", listWorkspaceMembershipsRoute);
4427
+ router8.get("/:id/RoleAssignment", listWorkspaceRoleAssignmentsRoute);
4428
+ router8.get("/:id/Configuration", listWorkspaceConfigurationsRoute);
2720
4429
 
2721
4430
  // src/data/rest-api/routes/data/account/account.ts
2722
4431
  import express9 from "express";
@@ -3727,6 +5436,7 @@ var DataApiPostgresQueryRunner = class {
3727
5436
  this.schema = options.schema;
3728
5437
  }
3729
5438
  async query(sql, params) {
5439
+ await this.ensureSchemaBootstrapped();
3730
5440
  const out = await this.client.send(
3731
5441
  new ExecuteStatementCommand({
3732
5442
  resourceArn: this.clusterArn,
@@ -3752,6 +5462,42 @@ var DataApiPostgresQueryRunner = class {
3752
5462
  }
3753
5463
  return JSON.parse(out.formattedRecords);
3754
5464
  }
5465
+ /**
5466
+ * Run `CREATE SCHEMA / CREATE TABLE / CREATE INDEX` idempotent DDL once per
5467
+ * runner instance. The replication Lambda's cold start does the same thing
5468
+ * (data-store-postgres-replication.handler) but in a fresh environment with
5469
+ * no DynamoDB writes yet the read path can be the first thing that touches
5470
+ * the cluster — without this, every search returns
5471
+ * `relation "<schema>.resources" does not exist` until something has been
5472
+ * written. Memoized so subsequent queries on a warm container don't pay the
5473
+ * DDL cost; resets the promise on failure so the next call retries.
5474
+ *
5475
+ * Data API's `ExecuteStatement` accepts only a single SQL statement per
5476
+ * call, so each DDL statement from `buildSchemaBootstrapStatements` is
5477
+ * issued individually. `IF NOT EXISTS` on every statement makes this safe
5478
+ * to race across concurrent containers.
5479
+ */
5480
+ async ensureSchemaBootstrapped() {
5481
+ if (!this.bootstrapPromise) {
5482
+ this.bootstrapPromise = this.runBootstrap().catch((err) => {
5483
+ this.bootstrapPromise = void 0;
5484
+ throw err;
5485
+ });
5486
+ }
5487
+ return this.bootstrapPromise;
5488
+ }
5489
+ async runBootstrap() {
5490
+ for (const statement of buildSchemaBootstrapStatements(this.schema)) {
5491
+ await this.client.send(
5492
+ new ExecuteStatementCommand({
5493
+ resourceArn: this.clusterArn,
5494
+ secretArn: this.secretArn,
5495
+ database: this.database,
5496
+ sql: statement
5497
+ })
5498
+ );
5499
+ }
5500
+ }
3755
5501
  };
3756
5502
  function qualifyResourcesTable(sql, schema) {
3757
5503
  return sql.replace(