@openhi/constructs 0.0.111 → 0.0.113
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.
- package/lib/chunk-23PUSHBV.mjs +24 -0
- package/lib/chunk-23PUSHBV.mjs.map +1 -0
- package/lib/{chunk-7FUAMZOF.mjs → chunk-53OHXLIL.mjs} +3 -3
- package/lib/chunk-6NBGYGFL.mjs +1803 -0
- package/lib/chunk-6NBGYGFL.mjs.map +1 -0
- package/lib/chunk-7RZHFI77.mjs +22 -0
- package/lib/chunk-7RZHFI77.mjs.map +1 -0
- package/lib/{chunk-7Q2IJ2J5.mjs → chunk-CUUKXDB2.mjs} +6 -6
- package/lib/chunk-FYHBHHWK.mjs +47 -0
- package/lib/chunk-FYHBHHWK.mjs.map +1 -0
- package/lib/{chunk-MULKGFIJ.mjs → chunk-GBDIGTNV.mjs} +165 -10
- package/lib/chunk-GBDIGTNV.mjs.map +1 -0
- package/lib/chunk-HQ67J7BP.mjs +199 -0
- package/lib/chunk-HQ67J7BP.mjs.map +1 -0
- package/lib/{chunk-AJ3G3THO.mjs → chunk-KO64HPWQ.mjs} +2 -2
- package/lib/{chunk-BB5MK4L3.mjs → chunk-KSFC72TT.mjs} +3 -3
- package/lib/{chunk-2TPJ6HOF.mjs → chunk-NZRW7ROK.mjs} +72 -54
- package/lib/chunk-NZRW7ROK.mjs.map +1 -0
- package/lib/chunk-QJDHVMKT.mjs +117 -0
- package/lib/chunk-QJDHVMKT.mjs.map +1 -0
- package/lib/{chunk-IS4VQRI4.mjs → chunk-QMBJ4VHC.mjs} +12 -47
- package/lib/chunk-QMBJ4VHC.mjs.map +1 -0
- package/lib/chunk-TRY7JGWO.mjs +16 -0
- package/lib/chunk-TRY7JGWO.mjs.map +1 -0
- package/lib/chunk-W4KR4CSL.mjs +236 -0
- package/lib/chunk-W4KR4CSL.mjs.map +1 -0
- package/lib/{chunk-AGF3RAAZ.mjs → chunk-WPCBVDFZ.mjs} +2 -2
- package/lib/chunk-WQWFVEVX.mjs +66 -0
- package/lib/chunk-WQWFVEVX.mjs.map +1 -0
- package/lib/{chunk-SYBADQXI.mjs → chunk-ZM4GDHHC.mjs} +77 -2
- package/lib/chunk-ZM4GDHHC.mjs.map +1 -0
- package/lib/delete-chunk.handler.d.mts +29 -0
- package/lib/delete-chunk.handler.d.ts +29 -0
- package/lib/delete-chunk.handler.js +2716 -0
- package/lib/delete-chunk.handler.js.map +1 -0
- package/lib/delete-chunk.handler.mjs +47 -0
- package/lib/delete-chunk.handler.mjs.map +1 -0
- package/lib/events-CjS-sm0W.d.mts +107 -0
- package/lib/events-CjS-sm0W.d.ts +107 -0
- package/lib/events-Da_cFgtc.d.mts +208 -0
- package/lib/events-Da_cFgtc.d.ts +208 -0
- package/lib/finalize.handler.d.mts +35 -0
- package/lib/finalize.handler.d.ts +35 -0
- package/lib/finalize.handler.js +875 -0
- package/lib/finalize.handler.js.map +1 -0
- package/lib/finalize.handler.mjs +166 -0
- package/lib/finalize.handler.mjs.map +1 -0
- package/lib/index.d.mts +189 -2
- package/lib/index.d.ts +500 -3
- package/lib/index.js +1753 -174
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +571 -17
- package/lib/index.mjs.map +1 -1
- package/lib/list-chunks.handler.d.mts +28 -0
- package/lib/list-chunks.handler.d.ts +28 -0
- package/lib/list-chunks.handler.js +2746 -0
- package/lib/list-chunks.handler.js.map +1 -0
- package/lib/list-chunks.handler.mjs +54 -0
- package/lib/list-chunks.handler.mjs.map +1 -0
- package/lib/platform-deploy-bridge.handler.js +76 -1
- package/lib/platform-deploy-bridge.handler.js.map +1 -1
- package/lib/platform-deploy-bridge.handler.mjs +1 -1
- package/lib/pre-token-generation.handler.js +1106 -155
- package/lib/pre-token-generation.handler.js.map +1 -1
- package/lib/pre-token-generation.handler.mjs +6 -4
- package/lib/pre-token-generation.handler.mjs.map +1 -1
- package/lib/provision-default-workspace.handler.js +1529 -142
- package/lib/provision-default-workspace.handler.js.map +1 -1
- package/lib/provision-default-workspace.handler.mjs +8 -4
- package/lib/provision-default-workspace.handler.mjs.map +1 -1
- package/lib/rename-finalize.handler.d.mts +30 -0
- package/lib/rename-finalize.handler.d.ts +30 -0
- package/lib/rename-finalize.handler.js +795 -0
- package/lib/rename-finalize.handler.js.map +1 -0
- package/lib/rename-finalize.handler.mjs +90 -0
- package/lib/rename-finalize.handler.mjs.map +1 -0
- package/lib/rename-list-targets.handler.d.mts +26 -0
- package/lib/rename-list-targets.handler.d.ts +26 -0
- package/lib/rename-list-targets.handler.js +2985 -0
- package/lib/rename-list-targets.handler.js.map +1 -0
- package/lib/rename-list-targets.handler.mjs +431 -0
- package/lib/rename-list-targets.handler.mjs.map +1 -0
- package/lib/rename-rewrite-chunk.handler.d.mts +35 -0
- package/lib/rename-rewrite-chunk.handler.d.ts +35 -0
- package/lib/rename-rewrite-chunk.handler.js +2021 -0
- package/lib/rename-rewrite-chunk.handler.js.map +1 -0
- package/lib/rename-rewrite-chunk.handler.mjs +27 -0
- package/lib/rename-rewrite-chunk.handler.mjs.map +1 -0
- package/lib/rest-api-lambda.handler.js +4021 -932
- package/lib/rest-api-lambda.handler.js.map +1 -1
- package/lib/rest-api-lambda.handler.mjs +1786 -80
- package/lib/rest-api-lambda.handler.mjs.map +1 -1
- package/lib/seed-demo-data.handler.js +1588 -124
- package/lib/seed-demo-data.handler.js.map +1 -1
- package/lib/seed-demo-data.handler.mjs +10 -6
- package/lib/seed-system-data.handler.js +1179 -155
- package/lib/seed-system-data.handler.js.map +1 -1
- package/lib/seed-system-data.handler.mjs +5 -4
- package/lib/seed-system-data.handler.mjs.map +1 -1
- package/package.json +2 -2
- package/lib/chunk-2TPJ6HOF.mjs.map +0 -1
- package/lib/chunk-IS4VQRI4.mjs.map +0 -1
- package/lib/chunk-MULKGFIJ.mjs.map +0 -1
- package/lib/chunk-QR5JVSCF.mjs +0 -862
- package/lib/chunk-QR5JVSCF.mjs.map +0 -1
- package/lib/chunk-SYBADQXI.mjs.map +0 -1
- /package/lib/{chunk-7FUAMZOF.mjs.map → chunk-53OHXLIL.mjs.map} +0 -0
- /package/lib/{chunk-7Q2IJ2J5.mjs.map → chunk-CUUKXDB2.mjs.map} +0 -0
- /package/lib/{chunk-AJ3G3THO.mjs.map → chunk-KO64HPWQ.mjs.map} +0 -0
- /package/lib/{chunk-BB5MK4L3.mjs.map → chunk-KSFC72TT.mjs.map} +0 -0
- /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-
|
|
9
|
+
} from "./chunk-53OHXLIL.mjs";
|
|
10
10
|
import {
|
|
11
11
|
listMembershipsOperation,
|
|
12
12
|
listPractitionerRolesOperation,
|
|
13
13
|
listRoleAssignmentsOperation
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-KSFC72TT.mjs";
|
|
15
15
|
import {
|
|
16
16
|
createMembershipOperation,
|
|
17
17
|
createRoleAssignmentOperation,
|
|
18
18
|
createTenantOperation,
|
|
19
|
-
createWorkspaceOperation
|
|
20
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
}
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
2240
|
-
async function
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
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/
|
|
2251
|
-
async function
|
|
2252
|
-
const
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
}
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
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
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
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/
|
|
2291
|
-
var
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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";
|