@openhi/constructs 0.0.159 → 0.0.161
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/{chunk-HQ67J7BP.mjs → chunk-5S6VFBLT.mjs} +12 -70
- package/lib/chunk-5S6VFBLT.mjs.map +1 -0
- package/lib/{chunk-MVQWAIMC.mjs → chunk-6BB4CRSS.mjs} +3 -312
- package/lib/chunk-6BB4CRSS.mjs.map +1 -0
- package/lib/{chunk-WPCBVDFZ.mjs → chunk-76UM2LQ5.mjs} +2 -2
- package/lib/chunk-7TRO2STL.mjs +4616 -0
- package/lib/chunk-7TRO2STL.mjs.map +1 -0
- package/lib/chunk-BUAYVN3C.mjs +87 -0
- package/lib/chunk-BUAYVN3C.mjs.map +1 -0
- package/lib/{chunk-23PUSHBV.mjs → chunk-D2Y6DDOC.mjs} +2 -2
- package/lib/chunk-DWSWCUZR.mjs +123 -0
- package/lib/chunk-DWSWCUZR.mjs.map +1 -0
- package/lib/{chunk-VZCPGQXA.mjs → chunk-EUIP2U5F.mjs} +69 -1
- package/lib/{chunk-VZCPGQXA.mjs.map → chunk-EUIP2U5F.mjs.map} +1 -1
- package/lib/chunk-GJTPXJKD.mjs +46 -0
- package/lib/chunk-GJTPXJKD.mjs.map +1 -0
- package/lib/chunk-I6LUPJUY.mjs +61 -0
- package/lib/chunk-I6LUPJUY.mjs.map +1 -0
- package/lib/{chunk-KR2Y2CVQ.mjs → chunk-KA3OMP3X.mjs} +2 -2
- package/lib/{chunk-ZM4GDHHC.mjs → chunk-KMEWULMX.mjs} +51 -3
- package/lib/chunk-KMEWULMX.mjs.map +1 -0
- package/lib/chunk-LKKLO66E.mjs +25 -0
- package/lib/chunk-LKKLO66E.mjs.map +1 -0
- package/lib/{chunk-CFJDATDK.mjs → chunk-MLFMW5IF.mjs} +43 -9
- package/lib/chunk-MLFMW5IF.mjs.map +1 -0
- package/lib/chunk-O5VQWB6U.mjs +315 -0
- package/lib/chunk-O5VQWB6U.mjs.map +1 -0
- package/lib/{chunk-7BQHLC7U.mjs → chunk-P3CTZWC2.mjs} +8 -40
- package/lib/chunk-P3CTZWC2.mjs.map +1 -0
- package/lib/chunk-P3NFCKTZ.mjs +502 -0
- package/lib/chunk-P3NFCKTZ.mjs.map +1 -0
- package/lib/{chunk-M7Y3BOQW.mjs → chunk-Q3MKITPY.mjs} +5 -5
- package/lib/chunk-Q64MOYJ7.mjs +218 -0
- package/lib/chunk-Q64MOYJ7.mjs.map +1 -0
- package/lib/chunk-RQKJNMX5.mjs +89 -0
- package/lib/chunk-RQKJNMX5.mjs.map +1 -0
- package/lib/{chunk-ZWSGM6PZ.mjs → chunk-SD7J3N3C.mjs} +2 -2
- package/lib/{chunk-7RZHFI77.mjs → chunk-VESULYQQ.mjs} +2 -2
- package/lib/{chunk-AOSEKL7U.mjs → chunk-WOTU36P3.mjs} +6 -103
- package/lib/chunk-WOTU36P3.mjs.map +1 -0
- package/lib/{chunk-X5E4YJGZ.mjs → chunk-YPTJJ35S.mjs} +2 -2
- package/lib/counter-apply-operation-DZM3MIDm.d.mts +63 -0
- package/lib/counter-apply-operation-DZM3MIDm.d.ts +63 -0
- package/lib/counter-maintenance.handler.d.mts +38 -0
- package/lib/counter-maintenance.handler.d.ts +38 -0
- package/lib/counter-maintenance.handler.js +2885 -0
- package/lib/counter-maintenance.handler.js.map +1 -0
- package/lib/counter-maintenance.handler.mjs +180 -0
- package/lib/counter-maintenance.handler.mjs.map +1 -0
- package/lib/counter-reconciliation.handler.d.mts +116 -0
- package/lib/counter-reconciliation.handler.d.ts +116 -0
- package/lib/counter-reconciliation.handler.js +3324 -0
- package/lib/counter-reconciliation.handler.js.map +1 -0
- package/lib/counter-reconciliation.handler.mjs +295 -0
- package/lib/counter-reconciliation.handler.mjs.map +1 -0
- package/lib/data-store-postgres-replication.handler.js +50 -2
- package/lib/data-store-postgres-replication.handler.js.map +1 -1
- package/lib/data-store-postgres-replication.handler.mjs +2 -2
- package/lib/delete-chunk.handler.js +118 -2
- package/lib/delete-chunk.handler.js.map +1 -1
- package/lib/delete-chunk.handler.mjs +3 -3
- package/lib/{events-DTgo2dcW.d.mts → events-TG654e7L.d.mts} +68 -19
- package/lib/{events-DTgo2dcW.d.ts → events-TG654e7L.d.ts} +68 -19
- package/lib/finalize.handler.js +50 -2
- package/lib/finalize.handler.js.map +1 -1
- package/lib/finalize.handler.mjs +4 -4
- package/lib/firehose-archive-transform.handler.js +50 -2
- package/lib/firehose-archive-transform.handler.js.map +1 -1
- package/lib/firehose-archive-transform.handler.mjs +2 -2
- package/lib/index.d.mts +1283 -4
- package/lib/index.d.ts +1389 -24
- package/lib/index.js +4113 -320
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +602 -195
- package/lib/index.mjs.map +1 -1
- package/lib/list-chunks.handler.js +118 -2
- package/lib/list-chunks.handler.js.map +1 -1
- package/lib/list-chunks.handler.mjs +3 -3
- package/lib/platform-deploy-bridge.handler.js +50 -2
- 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 +68 -0
- package/lib/pre-token-generation.handler.js.map +1 -1
- package/lib/pre-token-generation.handler.mjs +9 -5
- package/lib/pre-token-generation.handler.mjs.map +1 -1
- package/lib/provision-default-workspace.handler.js +887 -4
- package/lib/provision-default-workspace.handler.js.map +1 -1
- package/lib/provision-default-workspace.handler.mjs +14 -9
- package/lib/provision-default-workspace.handler.mjs.map +1 -1
- package/lib/rename-finalize.handler.js +50 -2
- package/lib/rename-finalize.handler.js.map +1 -1
- package/lib/rename-finalize.handler.mjs +2 -2
- package/lib/rename-list-targets.handler.js +118 -2
- package/lib/rename-list-targets.handler.js.map +1 -1
- package/lib/rename-list-targets.handler.mjs +11 -9
- package/lib/rename-list-targets.handler.mjs.map +1 -1
- package/lib/rename-rewrite-chunk.handler.js +68 -0
- package/lib/rename-rewrite-chunk.handler.js.map +1 -1
- package/lib/rename-rewrite-chunk.handler.mjs +2 -2
- package/lib/rest-api-lambda.handler.js +1454 -251
- package/lib/rest-api-lambda.handler.js.map +1 -1
- package/lib/rest-api-lambda.handler.mjs +673 -821
- package/lib/rest-api-lambda.handler.mjs.map +1 -1
- package/lib/seed-demo-data.handler.d.mts +1 -1
- package/lib/seed-demo-data.handler.d.ts +1 -1
- package/lib/seed-demo-data.handler.js +4004 -201
- package/lib/seed-demo-data.handler.js.map +1 -1
- package/lib/seed-demo-data.handler.mjs +10 -7
- package/lib/seed-system-data.handler.js +118 -2
- package/lib/seed-system-data.handler.js.map +1 -1
- package/lib/seed-system-data.handler.mjs +5 -5
- package/package.json +1 -1
- package/lib/chunk-7BQHLC7U.mjs.map +0 -1
- package/lib/chunk-AOSEKL7U.mjs.map +0 -1
- package/lib/chunk-BQMJSDOD.mjs +0 -1136
- package/lib/chunk-BQMJSDOD.mjs.map +0 -1
- package/lib/chunk-CFJDATDK.mjs.map +0 -1
- package/lib/chunk-E6MCKJVS.mjs +0 -212
- package/lib/chunk-E6MCKJVS.mjs.map +0 -1
- package/lib/chunk-HQ67J7BP.mjs.map +0 -1
- package/lib/chunk-MVQWAIMC.mjs.map +0 -1
- package/lib/chunk-ZM4GDHHC.mjs.map +0 -1
- /package/lib/{chunk-WPCBVDFZ.mjs.map → chunk-76UM2LQ5.mjs.map} +0 -0
- /package/lib/{chunk-23PUSHBV.mjs.map → chunk-D2Y6DDOC.mjs.map} +0 -0
- /package/lib/{chunk-KR2Y2CVQ.mjs.map → chunk-KA3OMP3X.mjs.map} +0 -0
- /package/lib/{chunk-M7Y3BOQW.mjs.map → chunk-Q3MKITPY.mjs.map} +0 -0
- /package/lib/{chunk-ZWSGM6PZ.mjs.map → chunk-SD7J3N3C.mjs.map} +0 -0
- /package/lib/{chunk-7RZHFI77.mjs.map → chunk-VESULYQQ.mjs.map} +0 -0
- /package/lib/{chunk-X5E4YJGZ.mjs.map → chunk-YPTJJ35S.mjs.map} +0 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildSkPrefix
|
|
3
|
+
} from "./chunk-DWSWCUZR.mjs";
|
|
4
|
+
import {
|
|
5
|
+
batchGetWithRetry,
|
|
6
|
+
dispatchListMode
|
|
7
|
+
} from "./chunk-O5VQWB6U.mjs";
|
|
8
|
+
import {
|
|
9
|
+
SHARD_COUNT,
|
|
10
|
+
getDynamoControlService
|
|
11
|
+
} from "./chunk-EUIP2U5F.mjs";
|
|
12
|
+
|
|
13
|
+
// src/data/operations/control/tenant/tenant-list-operation.ts
|
|
14
|
+
var SK = "CURRENT";
|
|
15
|
+
function counterValue(value) {
|
|
16
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
17
|
+
}
|
|
18
|
+
async function listTenantsOperation(params) {
|
|
19
|
+
const { tableName, mode = "full" } = params;
|
|
20
|
+
const service = getDynamoControlService(tableName);
|
|
21
|
+
const shardResults = await Promise.all(
|
|
22
|
+
Array.from(
|
|
23
|
+
{ length: SHARD_COUNT },
|
|
24
|
+
(_, shard) => service.entities.tenant.query.gsi1({ gsi1Shard: String(shard) }).go()
|
|
25
|
+
)
|
|
26
|
+
);
|
|
27
|
+
return dispatchListMode(mode, shardResults, {
|
|
28
|
+
hydrate: (orderedIds) => batchGetWithRetry(
|
|
29
|
+
service.entities.tenant,
|
|
30
|
+
orderedIds.map((id) => ({ tenantId: id, sk: SK }))
|
|
31
|
+
),
|
|
32
|
+
getId: (item) => item.id,
|
|
33
|
+
// FULL mode (admin list default): read the ADR-028 counters off the
|
|
34
|
+
// canonical record hydrated by BatchGet and expose them as
|
|
35
|
+
// `resource.counts`. Missing counters render as 0.
|
|
36
|
+
buildEntry: (id, item) => ({
|
|
37
|
+
id,
|
|
38
|
+
resource: {
|
|
39
|
+
resourceType: "Tenant",
|
|
40
|
+
id,
|
|
41
|
+
...JSON.parse(item.resource),
|
|
42
|
+
counts: {
|
|
43
|
+
usersInTenant: counterValue(item.usersInTenant),
|
|
44
|
+
workspacesInTenant: counterValue(item.workspacesInTenant)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}),
|
|
48
|
+
// SUMMARY mode reads only the GSI1 `summary` projection, which does
|
|
49
|
+
// not carry the counters; surface zeros so the shape stays uniform.
|
|
50
|
+
buildSummaryEntry: (id, parsed) => ({
|
|
51
|
+
id,
|
|
52
|
+
resource: {
|
|
53
|
+
resourceType: "Tenant",
|
|
54
|
+
id,
|
|
55
|
+
...parsed,
|
|
56
|
+
counts: { usersInTenant: 0, workspacesInTenant: 0 }
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/data/operations/control/workspace/workspace-list-operation.ts
|
|
63
|
+
var SK2 = "CURRENT";
|
|
64
|
+
function counterValue2(value) {
|
|
65
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
66
|
+
}
|
|
67
|
+
async function listWorkspacesOperation(params) {
|
|
68
|
+
const { context, tableName, mode = "full" } = params;
|
|
69
|
+
const { tenantId } = context;
|
|
70
|
+
const service = getDynamoControlService(tableName);
|
|
71
|
+
const shardResults = await Promise.all(
|
|
72
|
+
Array.from(
|
|
73
|
+
{ length: SHARD_COUNT },
|
|
74
|
+
(_, shard) => service.entities.workspace.query.gsi1({ tenantId, gsi1Shard: String(shard) }).go()
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
return dispatchListMode(mode, shardResults, {
|
|
78
|
+
hydrate: (orderedIds) => batchGetWithRetry(
|
|
79
|
+
service.entities.workspace,
|
|
80
|
+
orderedIds.map((id) => ({ tenantId, id, sk: SK2 }))
|
|
81
|
+
),
|
|
82
|
+
getId: (item) => item.id,
|
|
83
|
+
// FULL mode (admin list default): read the ADR-028 counters off the
|
|
84
|
+
// canonical record hydrated by BatchGet and expose them as
|
|
85
|
+
// `resource.counts`. Missing counters render as 0.
|
|
86
|
+
buildEntry: (id, item) => ({
|
|
87
|
+
id,
|
|
88
|
+
resource: {
|
|
89
|
+
resourceType: "Workspace",
|
|
90
|
+
id,
|
|
91
|
+
...JSON.parse(item.resource),
|
|
92
|
+
counts: {
|
|
93
|
+
usersInWorkspace: counterValue2(item.usersInWorkspace),
|
|
94
|
+
adminUsersInWorkspace: counterValue2(item.adminUsersInWorkspace),
|
|
95
|
+
normalUsersInWorkspace: counterValue2(item.normalUsersInWorkspace)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}),
|
|
99
|
+
// SUMMARY mode reads only the GSI1 `summary` projection (no
|
|
100
|
+
// counters); surface zeros so the shape stays uniform.
|
|
101
|
+
buildSummaryEntry: (id, parsed) => ({
|
|
102
|
+
id,
|
|
103
|
+
resource: {
|
|
104
|
+
resourceType: "Workspace",
|
|
105
|
+
id,
|
|
106
|
+
...parsed,
|
|
107
|
+
counts: {
|
|
108
|
+
usersInWorkspace: 0,
|
|
109
|
+
adminUsersInWorkspace: 0,
|
|
110
|
+
normalUsersInWorkspace: 0
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/data/operations/control/membership/membership-count-by-user-operation.ts
|
|
118
|
+
async function countMembershipsByUserOperation(params) {
|
|
119
|
+
const { userId, mode = "all", tenantId, tableName } = params;
|
|
120
|
+
if (mode === "workspaceInTenant" && !tenantId) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
'countMembershipsByUserOperation: tenantId is required when mode === "workspaceInTenant"'
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const service = getDynamoControlService(tableName);
|
|
126
|
+
const skPrefix = buildSkPrefix(mode, tenantId);
|
|
127
|
+
const result = await service.entities.membershipUserProjection.query.record({ userId }).begins({ sk: skPrefix }).go({ pages: "all", attributes: ["membershipId"] });
|
|
128
|
+
return (result.data ?? []).length;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/data/operations/control/membership/membership-list-by-workspace-operation.ts
|
|
132
|
+
async function membershipListByWorkspaceOperation(params) {
|
|
133
|
+
const {
|
|
134
|
+
tenantId,
|
|
135
|
+
workspaceId,
|
|
136
|
+
cursor = null,
|
|
137
|
+
limit,
|
|
138
|
+
order,
|
|
139
|
+
tableName
|
|
140
|
+
} = params;
|
|
141
|
+
const service = getDynamoControlService(tableName);
|
|
142
|
+
const goOptions = {
|
|
143
|
+
cursor
|
|
144
|
+
};
|
|
145
|
+
if (limit !== void 0) {
|
|
146
|
+
goOptions.limit = limit;
|
|
147
|
+
}
|
|
148
|
+
if (order !== void 0) {
|
|
149
|
+
goOptions.order = order;
|
|
150
|
+
}
|
|
151
|
+
const result = await service.entities.membershipWorkspaceProjection.query.record({ tenantId, workspaceId }).begins({ sk: "MEMBERSHIP#" }).go(goOptions);
|
|
152
|
+
const items = (result.data ?? []).map((row) => ({
|
|
153
|
+
tenantId: row.tenantId,
|
|
154
|
+
workspaceId: row.workspaceId,
|
|
155
|
+
sk: row.sk,
|
|
156
|
+
userId: row.userId,
|
|
157
|
+
membershipId: row.membershipId,
|
|
158
|
+
summary: row.summary,
|
|
159
|
+
vid: row.vid,
|
|
160
|
+
lastUpdated: row.lastUpdated,
|
|
161
|
+
denormalizedUserName: row.denormalizedUserName
|
|
162
|
+
}));
|
|
163
|
+
return { items, cursor: result.cursor ?? null };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/data/operations/control/roleassignment/roleassignment-list-by-workspace-operation.ts
|
|
167
|
+
function buildSkPrefix2(roleId) {
|
|
168
|
+
if (roleId === void 0 || roleId.length === 0) {
|
|
169
|
+
return "ROLEASSIGNMENT#";
|
|
170
|
+
}
|
|
171
|
+
return `ROLEASSIGNMENT#${roleId}#`;
|
|
172
|
+
}
|
|
173
|
+
async function roleAssignmentListByWorkspaceOperation(params) {
|
|
174
|
+
const {
|
|
175
|
+
tenantId,
|
|
176
|
+
workspaceId,
|
|
177
|
+
roleId,
|
|
178
|
+
cursor = null,
|
|
179
|
+
limit,
|
|
180
|
+
order,
|
|
181
|
+
tableName
|
|
182
|
+
} = params;
|
|
183
|
+
const service = getDynamoControlService(tableName);
|
|
184
|
+
const skPrefix = buildSkPrefix2(roleId);
|
|
185
|
+
const goOptions = {
|
|
186
|
+
cursor
|
|
187
|
+
};
|
|
188
|
+
if (limit !== void 0) {
|
|
189
|
+
goOptions.limit = limit;
|
|
190
|
+
}
|
|
191
|
+
if (order !== void 0) {
|
|
192
|
+
goOptions.order = order;
|
|
193
|
+
}
|
|
194
|
+
const result = await service.entities.roleAssignmentWorkspaceProjection.query.record({ tenantId, workspaceId }).begins({ sk: skPrefix }).go(goOptions);
|
|
195
|
+
const items = (result.data ?? []).map((row) => ({
|
|
196
|
+
tenantId: row.tenantId,
|
|
197
|
+
workspaceId: row.workspaceId,
|
|
198
|
+
sk: row.sk,
|
|
199
|
+
userId: row.userId,
|
|
200
|
+
roleId: row.roleId,
|
|
201
|
+
roleAssignmentId: row.roleAssignmentId,
|
|
202
|
+
summary: row.summary,
|
|
203
|
+
vid: row.vid,
|
|
204
|
+
lastUpdated: row.lastUpdated,
|
|
205
|
+
denormalizedUserName: row.denormalizedUserName,
|
|
206
|
+
denormalizedRoleName: row.denormalizedRoleName
|
|
207
|
+
}));
|
|
208
|
+
return { items, cursor: result.cursor ?? null };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export {
|
|
212
|
+
listTenantsOperation,
|
|
213
|
+
listWorkspacesOperation,
|
|
214
|
+
countMembershipsByUserOperation,
|
|
215
|
+
membershipListByWorkspaceOperation,
|
|
216
|
+
roleAssignmentListByWorkspaceOperation
|
|
217
|
+
};
|
|
218
|
+
//# sourceMappingURL=chunk-Q64MOYJ7.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/data/operations/control/tenant/tenant-list-operation.ts","../src/data/operations/control/workspace/workspace-list-operation.ts","../src/data/operations/control/membership/membership-count-by-user-operation.ts","../src/data/operations/control/membership/membership-list-by-workspace-operation.ts","../src/data/operations/control/roleassignment/roleassignment-list-by-workspace-operation.ts"],"sourcesContent":["import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { SHARD_COUNT } from \"../../../dynamo/shard\";\nimport type { OpenHiContext } from \"../../../openhi-context\";\nimport {\n batchGetWithRetry,\n dispatchListMode,\n type ListOperationMode,\n} from \"../../data-operations-common\";\n\nconst SK = \"CURRENT\";\n\nexport interface ListTenantsParams {\n context: OpenHiContext;\n tableName?: string;\n /** #853: defaults to `\"full\"`. `\"summary\"` skips BatchGet, `\"count\"` returns total only. */\n mode?: ListOperationMode;\n}\n\n/**\n * ADR-028 denormalized counter shape surfaced on a Tenant list entry's\n * `resource.counts`. Missing counters render as `0` so the admin console\n * never sees `undefined`.\n */\nexport interface TenantCounts {\n usersInTenant: number;\n workspacesInTenant: number;\n}\n\nexport interface TenantListEntry {\n id: string;\n resource: Record<string, unknown> & { counts: TenantCounts };\n}\n\nexport interface ListTenantsResult {\n entries: TenantListEntry[];\n total: number;\n}\n\n/** Coerce a possibly-absent counter attribute to a non-negative number (default 0). */\nfunction counterValue(value: unknown): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n}\n\n/**\n * Lists all Tenants (platform-wide, no scope filter) via GSI1 (sharded). Tenant uses tenantId\n * as its identity (tenantId === id), so BatchGet keys reuse `id` as `tenantId`. See\n * `dispatchListMode` for the mode contract (#853).\n */\nexport async function listTenantsOperation(\n params: ListTenantsParams,\n): Promise<ListTenantsResult> {\n const { tableName, mode = \"full\" } = params;\n const service = getDynamoControlService(tableName);\n\n const shardResults = await Promise.all(\n Array.from({ length: SHARD_COUNT }, (_, shard) =>\n service.entities.tenant.query.gsi1({ gsi1Shard: String(shard) }).go(),\n ),\n );\n\n return dispatchListMode<\n {\n id: string;\n resource: string;\n usersInTenant?: number;\n workspacesInTenant?: number;\n },\n TenantListEntry\n >(mode, shardResults, {\n hydrate: (orderedIds) =>\n batchGetWithRetry(\n service.entities.tenant,\n orderedIds.map((id) => ({ tenantId: id, sk: SK })),\n ) as Promise<\n Array<{\n id: string;\n resource: string;\n usersInTenant?: number;\n workspacesInTenant?: number;\n }>\n >,\n getId: (item) => item.id,\n // FULL mode (admin list default): read the ADR-028 counters off the\n // canonical record hydrated by BatchGet and expose them as\n // `resource.counts`. Missing counters render as 0.\n buildEntry: (id, item) => ({\n id,\n resource: {\n resourceType: \"Tenant\",\n id,\n ...(JSON.parse(item.resource) as Record<string, unknown>),\n counts: {\n usersInTenant: counterValue(item.usersInTenant),\n workspacesInTenant: counterValue(item.workspacesInTenant),\n },\n },\n }),\n // SUMMARY mode reads only the GSI1 `summary` projection, which does\n // not carry the counters; surface zeros so the shape stays uniform.\n buildSummaryEntry: (id, parsed) => ({\n id,\n resource: {\n resourceType: \"Tenant\",\n id,\n ...parsed,\n counts: { usersInTenant: 0, workspacesInTenant: 0 },\n },\n }),\n });\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { SHARD_COUNT } from \"../../../dynamo/shard\";\nimport type { OpenHiContext } from \"../../../openhi-context\";\nimport {\n batchGetWithRetry,\n dispatchListMode,\n type ListOperationMode,\n} from \"../../data-operations-common\";\n\nconst SK = \"CURRENT\";\n\nexport interface ListWorkspacesParams {\n context: OpenHiContext;\n tableName?: string;\n /** #853: defaults to `\"full\"`. `\"summary\"` skips BatchGet, `\"count\"` returns total only. */\n mode?: ListOperationMode;\n}\n\n/**\n * ADR-028 denormalized counter shape surfaced on a Workspace list\n * entry's `resource.counts`. Missing counters render as `0`.\n */\nexport interface WorkspaceCounts {\n usersInWorkspace: number;\n adminUsersInWorkspace: number;\n normalUsersInWorkspace: number;\n}\n\nexport interface WorkspaceListEntry {\n id: string;\n resource: Record<string, unknown> & { counts: WorkspaceCounts };\n}\n\nexport interface ListWorkspacesResult {\n entries: WorkspaceListEntry[];\n total: number;\n}\n\n/** Coerce a possibly-absent counter attribute to a non-negative number (default 0). */\nfunction counterValue(value: unknown): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n}\n\n/**\n * Lists all Workspaces for the context tenant via GSI1 (sharded). See `dispatchListMode` for\n * the mode contract (#853).\n */\nexport async function listWorkspacesOperation(\n params: ListWorkspacesParams,\n): Promise<ListWorkspacesResult> {\n const { context, tableName, mode = \"full\" } = params;\n const { tenantId } = context;\n const service = getDynamoControlService(tableName);\n\n const shardResults = await Promise.all(\n Array.from({ length: SHARD_COUNT }, (_, shard) =>\n service.entities.workspace.query\n .gsi1({ tenantId, gsi1Shard: String(shard) })\n .go(),\n ),\n );\n\n return dispatchListMode<\n {\n id: string;\n resource: string;\n usersInWorkspace?: number;\n adminUsersInWorkspace?: number;\n normalUsersInWorkspace?: number;\n },\n WorkspaceListEntry\n >(mode, shardResults, {\n hydrate: (orderedIds) =>\n batchGetWithRetry(\n service.entities.workspace,\n orderedIds.map((id) => ({ tenantId, id, sk: SK })),\n ) as Promise<\n Array<{\n id: string;\n resource: string;\n usersInWorkspace?: number;\n adminUsersInWorkspace?: number;\n normalUsersInWorkspace?: number;\n }>\n >,\n getId: (item) => item.id,\n // FULL mode (admin list default): read the ADR-028 counters off the\n // canonical record hydrated by BatchGet and expose them as\n // `resource.counts`. Missing counters render as 0.\n buildEntry: (id, item) => ({\n id,\n resource: {\n resourceType: \"Workspace\",\n id,\n ...(JSON.parse(item.resource) as Record<string, unknown>),\n counts: {\n usersInWorkspace: counterValue(item.usersInWorkspace),\n adminUsersInWorkspace: counterValue(item.adminUsersInWorkspace),\n normalUsersInWorkspace: counterValue(item.normalUsersInWorkspace),\n },\n },\n }),\n // SUMMARY mode reads only the GSI1 `summary` projection (no\n // counters); surface zeros so the shape stays uniform.\n buildSummaryEntry: (id, parsed) => ({\n id,\n resource: {\n resourceType: \"Workspace\",\n id,\n ...parsed,\n counts: {\n usersInWorkspace: 0,\n adminUsersInWorkspace: 0,\n normalUsersInWorkspace: 0,\n },\n },\n }),\n });\n}\n","import {\n buildSkPrefix,\n type MembershipListByUserMode,\n} from \"./membership-list-by-user-operation\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\n\n/** Inputs accepted by {@link countMembershipsByUserOperation}. */\nexport interface CountMembershipsByUserParams {\n readonly userId: string;\n /** Filter mode — see {@link MembershipListByUserMode}. Defaults to `\"all\"`. */\n readonly mode?: MembershipListByUserMode;\n /** Required only when `mode === \"workspaceInTenant\"`. */\n readonly tenantId?: string;\n /** Optional table-name override; resolved via env when omitted. */\n readonly tableName?: string;\n}\n\n/**\n * Count a user's Memberships via the ADR-018 user-partition projection, with the\n * same `SK begins_with` lane discriminator as {@link membershipListByUserOperation}.\n *\n * Pages through every matching projection row (`{ pages: \"all\" }`) projecting only\n * the `membershipId` key, and returns the row count. Backs\n * `GET /User/:id/Membership?_summary=count`.\n *\n * Note: the per-user counts (`tenantsForUser`, `workspacesForUser`) are also\n * maintained as denormalized counters on the User record per ADR-028; this\n * recompute-from-projection count is the FHIR `_summary=count` shape and the\n * authoritative source the counters are reconciled against.\n */\nexport async function countMembershipsByUserOperation(\n params: CountMembershipsByUserParams,\n): Promise<number> {\n const { userId, mode = \"all\", tenantId, tableName } = params;\n\n if (mode === \"workspaceInTenant\" && !tenantId) {\n throw new Error(\n 'countMembershipsByUserOperation: tenantId is required when mode === \"workspaceInTenant\"',\n );\n }\n\n const service = getDynamoControlService(tableName);\n const skPrefix = buildSkPrefix(mode, tenantId);\n\n const result = await service.entities.membershipUserProjection.query\n .record({ userId })\n .begins({ sk: skPrefix })\n .go({ pages: \"all\", attributes: [\"membershipId\"] });\n\n return (result.data ?? []).length;\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\n\n/** Inputs accepted by {@link membershipListByWorkspaceOperation}. */\nexport interface MembershipListByWorkspaceParams {\n readonly tenantId: string;\n readonly workspaceId: string;\n /** ElectroDB cursor from a prior page. Forwarded to `.go({ cursor })`. */\n readonly cursor?: string | null;\n /** Per-page item limit forwarded to `.go({ limit })`. */\n readonly limit?: number;\n /** Sort order forwarded to `.go({ order })`. Defaults to ElectroDB's `\"asc\"`. */\n readonly order?: \"asc\" | \"desc\";\n /** Optional table-name override; resolved via env when omitted. */\n readonly tableName?: string;\n}\n\n/** One projection-row payload as returned to a consumer. */\nexport interface MembershipWorkspaceProjectionEntry {\n readonly tenantId: string;\n readonly workspaceId: string;\n readonly sk: string;\n readonly userId: string;\n readonly membershipId: string;\n readonly summary: string;\n readonly vid: string;\n readonly lastUpdated: string;\n readonly denormalizedUserName?: string;\n}\n\n/** Page returned by {@link membershipListByWorkspaceOperation}. */\nexport interface MembershipListByWorkspaceResult {\n readonly items: Array<MembershipWorkspaceProjectionEntry>;\n /** ElectroDB cursor for the next page, or `null` when exhausted. */\n readonly cursor: string | null;\n}\n\n/**\n * List Memberships for a workspace via the ADR-018 workspace-partition\n * projection (no GSI hop).\n *\n * Reads `MembershipWorkspaceProjectionEntity` rows under\n * `PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>` with\n * `SK begins_with 'MEMBERSHIP#'`. The projection's SK shape\n * (`MEMBERSHIP#<normalizedUserName>#USER#<userId>#<membershipId>`) sorts\n * members alphabetically by user name within the workspace partition, so\n * the natural Query order is exactly what the access pattern expects —\n * no client-side sort.\n *\n * Returns the projection rows verbatim (`summary`, `vid`, `lastUpdated`\n * plus the projection-discriminating fields) — full canonical-resource\n * hydration is opt-in for callers via\n * `MembershipEntity.get({ tenantId, id: membershipId })`. Pagination\n * mirrors ElectroDB's native `.go({ cursor })` shape; the returned\n * `cursor` is opaque to callers.\n *\n * @see ADR-018 § Access Pattern Coverage (pattern #2)\n * @see .state/adr-018-implementation-guide.md § 1 (SK grammar)\n */\nexport async function membershipListByWorkspaceOperation(\n params: MembershipListByWorkspaceParams,\n): Promise<MembershipListByWorkspaceResult> {\n const {\n tenantId,\n workspaceId,\n cursor = null,\n limit,\n order,\n tableName,\n } = params;\n\n const service = getDynamoControlService(tableName);\n\n const goOptions: {\n cursor?: string | null;\n limit?: number;\n order?: \"asc\" | \"desc\";\n } = {\n cursor,\n };\n if (limit !== undefined) {\n goOptions.limit = limit;\n }\n if (order !== undefined) {\n goOptions.order = order;\n }\n\n const result = await service.entities.membershipWorkspaceProjection.query\n .record({ tenantId, workspaceId })\n .begins({ sk: \"MEMBERSHIP#\" })\n .go(goOptions);\n\n const items: Array<MembershipWorkspaceProjectionEntry> = (\n result.data ?? []\n ).map((row) => ({\n tenantId: row.tenantId,\n workspaceId: row.workspaceId,\n sk: row.sk,\n userId: row.userId,\n membershipId: row.membershipId,\n summary: row.summary,\n vid: row.vid,\n lastUpdated: row.lastUpdated,\n denormalizedUserName: row.denormalizedUserName,\n }));\n\n return { items, cursor: result.cursor ?? null };\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\n\n/** Inputs accepted by {@link roleAssignmentListByWorkspaceOperation}. */\nexport interface RoleAssignmentListByWorkspaceParams {\n readonly tenantId: string;\n readonly workspaceId: string;\n /**\n * Optional role discriminator. When supplied, narrows the listing to\n * users assigned to a single role via the discriminator-first\n * `begins_with('ROLEASSIGNMENT#<roleId>#')` prefix — the central read\n * pattern (#9 is \"users with role X in workspace Y\"). Omit to return\n * every role assignment in the workspace interleaved by `<roleId>`.\n */\n readonly roleId?: string;\n /** ElectroDB cursor from a prior page. Forwarded to `.go({ cursor })`. */\n readonly cursor?: string | null;\n /** Per-page item limit forwarded to `.go({ limit })`. */\n readonly limit?: number;\n /** Sort order forwarded to `.go({ order })`. Defaults to ElectroDB's `\"asc\"`. */\n readonly order?: \"asc\" | \"desc\";\n /** Optional table-name override; resolved via env when omitted. */\n readonly tableName?: string;\n}\n\n/** One projection-row payload as returned to a consumer. */\nexport interface RoleAssignmentWorkspaceProjectionEntry {\n readonly tenantId: string;\n readonly workspaceId: string;\n readonly sk: string;\n readonly userId: string;\n readonly roleId: string;\n readonly roleAssignmentId: string;\n readonly summary: string;\n readonly vid: string;\n readonly lastUpdated: string;\n readonly denormalizedUserName?: string;\n readonly denormalizedRoleName?: string;\n}\n\n/** Page returned by {@link roleAssignmentListByWorkspaceOperation}. */\nexport interface RoleAssignmentListByWorkspaceResult {\n readonly items: Array<RoleAssignmentWorkspaceProjectionEntry>;\n /** ElectroDB cursor for the next page, or `null` when exhausted. */\n readonly cursor: string | null;\n}\n\n/**\n * Compose the SK prefix for the workspace-projection list. The\n * pattern-#9 SK is **discriminator-first on the raw `<roleId>`**\n * (`ROLEASSIGNMENT#<roleId>#<normalizedUserName>#USER#<userId>#<id>`)\n * so a `begins_with('ROLEASSIGNMENT#<roleId>#')` filter returns every\n * user assigned to that role in the workspace. The trailing `#` after\n * the role id is critical — without it `ROLEASSIGNMENT#role-1` would\n * also match `ROLEASSIGNMENT#role-10`, `role-100`, etc. Omitting the\n * `roleId` arg falls back to the wider `ROLEASSIGNMENT#` prefix.\n */\nfunction buildSkPrefix(roleId: string | undefined): string {\n if (roleId === undefined || roleId.length === 0) {\n return \"ROLEASSIGNMENT#\";\n }\n return `ROLEASSIGNMENT#${roleId}#`;\n}\n\n/**\n * List RoleAssignments for a workspace via the ADR-018 workspace-\n * partition projection (no GSI hop).\n *\n * Reads `RoleAssignmentWorkspaceProjectionEntity` rows under\n * `PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>` with an\n * `SK begins_with` filter:\n *\n * | `roleId` arg | SK begins_with | Covers |\n * |---|---|---|\n * | omitted | `ROLEASSIGNMENT#` | Every role assignment in the workspace, interleaved by `<roleId>` |\n * | supplied | `ROLEASSIGNMENT#<roleId>#` | Pattern #9 — every user assigned to that role, sorted alphabetically by `<normalizedUserName>` |\n *\n * The projection's SK shape\n * (`ROLEASSIGNMENT#<roleId>#<normalizedUserName>#USER#<userId>#<id>`)\n * is discriminator-first on the raw `<roleId>` (mirroring the canonical\n * GSI1SK from pattern #8) so the natural Query order is exactly what\n * the access pattern expects — no client-side sort. Tenant-scoped\n * RoleAssignments (no `workspaceId`) skip this projection entirely;\n * they live only in the user-projection's tenant sub-lane.\n *\n * Returns the projection rows verbatim (`summary`, `vid`, `lastUpdated`\n * plus the projection-discriminating fields) — full canonical-resource\n * hydration is opt-in for callers via\n * `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`.\n * Pagination mirrors ElectroDB's native `.go({ cursor })` shape; the\n * returned `cursor` is opaque to callers.\n *\n * @see ADR-018 § Access Pattern Coverage (pattern #9)\n * @see .state/adr-018-implementation-guide.md § 1 (SK grammar) and § 2 (attribute set)\n */\nexport async function roleAssignmentListByWorkspaceOperation(\n params: RoleAssignmentListByWorkspaceParams,\n): Promise<RoleAssignmentListByWorkspaceResult> {\n const {\n tenantId,\n workspaceId,\n roleId,\n cursor = null,\n limit,\n order,\n tableName,\n } = params;\n\n const service = getDynamoControlService(tableName);\n const skPrefix = buildSkPrefix(roleId);\n\n const goOptions: {\n cursor?: string | null;\n limit?: number;\n order?: \"asc\" | \"desc\";\n } = {\n cursor,\n };\n if (limit !== undefined) {\n goOptions.limit = limit;\n }\n if (order !== undefined) {\n goOptions.order = order;\n }\n\n const result = await service.entities.roleAssignmentWorkspaceProjection.query\n .record({ tenantId, workspaceId })\n .begins({ sk: skPrefix })\n .go(goOptions);\n\n const items: Array<RoleAssignmentWorkspaceProjectionEntry> = (\n result.data ?? []\n ).map((row) => ({\n tenantId: row.tenantId,\n workspaceId: row.workspaceId,\n sk: row.sk,\n userId: row.userId,\n roleId: row.roleId,\n roleAssignmentId: row.roleAssignmentId,\n summary: row.summary,\n vid: row.vid,\n lastUpdated: row.lastUpdated,\n denormalizedUserName: row.denormalizedUserName,\n denormalizedRoleName: row.denormalizedRoleName,\n }));\n\n return { items, cursor: result.cursor ?? null };\n}\n"],"mappings":"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","names":["SK","counterValue","buildSkPrefix"]}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getDynamoControlService
|
|
3
|
+
} from "./chunk-EUIP2U5F.mjs";
|
|
4
|
+
|
|
5
|
+
// src/data/operations/control/counters/counter-apply-operation.ts
|
|
6
|
+
var COUNTER_TARGET = {
|
|
7
|
+
Tenant: "Tenant",
|
|
8
|
+
Workspace: "Workspace",
|
|
9
|
+
User: "User"
|
|
10
|
+
};
|
|
11
|
+
async function applyCounterDeltaOperation(params) {
|
|
12
|
+
const { mutation, tableName } = params;
|
|
13
|
+
const service = getDynamoControlService(tableName);
|
|
14
|
+
const patch = buildPatch(service, mutation);
|
|
15
|
+
try {
|
|
16
|
+
if (mutation.delta > 0) {
|
|
17
|
+
await patch.add({ [mutation.attribute]: mutation.delta }).go();
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
await patch.add({ [mutation.attribute]: mutation.delta }).where((attr, { gt }) => gt(attr[mutation.attribute], 0)).go();
|
|
21
|
+
return true;
|
|
22
|
+
} catch (err) {
|
|
23
|
+
if (isConditionalCheckFailure(err)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function buildPatch(service, mutation) {
|
|
30
|
+
switch (mutation.target) {
|
|
31
|
+
case COUNTER_TARGET.Tenant:
|
|
32
|
+
return service.entities.tenant.patch({
|
|
33
|
+
tenantId: mutation.tenantId,
|
|
34
|
+
sk: "CURRENT"
|
|
35
|
+
});
|
|
36
|
+
case COUNTER_TARGET.Workspace:
|
|
37
|
+
return service.entities.workspace.patch({
|
|
38
|
+
tenantId: mutation.tenantId,
|
|
39
|
+
id: mutation.workspaceId,
|
|
40
|
+
sk: "CURRENT"
|
|
41
|
+
});
|
|
42
|
+
case COUNTER_TARGET.User:
|
|
43
|
+
return service.entities.user.patch({
|
|
44
|
+
id: mutation.userId,
|
|
45
|
+
sk: "CURRENT"
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function isConditionalCheckFailure(err) {
|
|
50
|
+
if (typeof err !== "object" || err === null) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const e = err;
|
|
54
|
+
if (e.name === "ConditionalCheckFailedException" || e.code === "ConditionalCheckFailedException" || e.cause?.name === "ConditionalCheckFailedException") {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return typeof e.message === "string" ? e.message.includes("ConditionalCheckFailed") : false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/data/operations/control/counters/role-admin-classification.ts
|
|
61
|
+
function isAdminRoleAssignment(input) {
|
|
62
|
+
if (codeIsAdminTier(input.roleLevel)) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (idMatchesAdmin(input.roleId)) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
function codeIsAdminTier(roleLevel) {
|
|
71
|
+
if (typeof roleLevel !== "string" || roleLevel.length === 0) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const lower = roleLevel.toLowerCase();
|
|
75
|
+
return lower === "admin" || lower.endsWith("admin");
|
|
76
|
+
}
|
|
77
|
+
function idMatchesAdmin(roleId) {
|
|
78
|
+
if (typeof roleId !== "string" || roleId.length === 0) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return roleId.toLowerCase().includes("admin");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
COUNTER_TARGET,
|
|
86
|
+
applyCounterDeltaOperation,
|
|
87
|
+
isAdminRoleAssignment
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=chunk-RQKJNMX5.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/data/operations/control/counters/counter-apply-operation.ts","../src/data/operations/control/counters/role-admin-classification.ts"],"sourcesContent":["import {\n getDynamoControlService,\n type DynamoControlServiceType,\n} from \"../../../dynamo/dynamo-control-service\";\n\n/**\n * ADR-028 atomic counter mutation against a canonical control-plane\n * record. Each call applies one concurrency-safe DynamoDB `ADD` delta\n * (`+1` on a relationship/lifecycle create, `-1` on delete) to a single\n * named counter attribute on the canonical Tenant / Workspace / User row.\n *\n * The mutation goes through ElectroDB's `patch(key).add({ counter })`,\n * which compiles to a DynamoDB `ADD` update expression — atomic, no\n * read-modify-write. `patch` also stamps an `attribute_exists` guard on\n * the partition key, so a counter is never written onto a phantom /\n * deleted canonical record. Using ElectroDB (rather than a raw\n * `UpdateItemCommand`) keeps the composite-key casing in lockstep with\n * the entity definitions.\n *\n * Floor guard contract (ADR-028 § Floor guard): a `-1` against an\n * absent / `0` counter is clamped to a no-op via a `.where(counter > 0)`\n * condition — when the condition fails ElectroDB throws and the\n * operation swallows it, returning `false`. The guard is a safety net,\n * not the source of truth: ADR-028's reconciliation job recomputes the\n * real value from canonical data and owns correctness.\n *\n * @see sites/www-docs/content/packages/@openhi/constructs/data/operations/control/counters/counter-apply-operation.md\n */\n\n/** Counter attribute names per ADR-028, grouped by the canonical record they live on. */\nexport const TENANT_COUNTERS = [\"usersInTenant\", \"workspacesInTenant\"] as const;\nexport const WORKSPACE_COUNTERS = [\n \"usersInWorkspace\",\n \"adminUsersInWorkspace\",\n \"normalUsersInWorkspace\",\n] as const;\nexport const USER_COUNTERS = [\"tenantsForUser\", \"workspacesForUser\"] as const;\n\nexport type TenantCounter = (typeof TENANT_COUNTERS)[number];\nexport type WorkspaceCounter = (typeof WORKSPACE_COUNTERS)[number];\nexport type UserCounter = (typeof USER_COUNTERS)[number];\n\n/** The `+1` (create) / `-1` (delete) delta direction. */\nexport type CounterDelta = 1 | -1;\n\n/** Which canonical entity the counter lives on. */\nexport const COUNTER_TARGET = {\n Tenant: \"Tenant\",\n Workspace: \"Workspace\",\n User: \"User\",\n} as const;\nexport type CounterTarget =\n (typeof COUNTER_TARGET)[keyof typeof COUNTER_TARGET];\n\n/**\n * A resolved counter mutation: which canonical record (target + the\n * ElectroDB composite-key fields that identify it), the attribute, and\n * the delta. The router emits these; the operation applies them.\n */\nexport type CounterMutation =\n | {\n readonly target: typeof COUNTER_TARGET.Tenant;\n readonly tenantId: string;\n readonly attribute: TenantCounter;\n readonly delta: CounterDelta;\n }\n | {\n readonly target: typeof COUNTER_TARGET.Workspace;\n readonly tenantId: string;\n readonly workspaceId: string;\n readonly attribute: WorkspaceCounter;\n readonly delta: CounterDelta;\n }\n | {\n readonly target: typeof COUNTER_TARGET.User;\n readonly userId: string;\n readonly attribute: UserCounter;\n readonly delta: CounterDelta;\n };\n\nexport interface ApplyCounterDeltaParams {\n readonly mutation: CounterMutation;\n /** Table override (tests); defaults to `DYNAMO_TABLE_NAME`. */\n readonly tableName?: string;\n}\n\n/** Minimal shape of the ElectroDB patch builder this operation drives. */\ninterface PatchBuilder {\n add(attrs: Record<string, number>): {\n where(\n cb: (\n attr: Record<string, unknown>,\n op: { gt: (a: unknown, b: number) => string },\n ) => string,\n ): { go(): Promise<unknown> };\n go(): Promise<unknown>;\n };\n}\n\n/**\n * Apply one atomic counter delta. Increments are unconditional `ADD`s\n * (guarded only by `patch`'s implicit `attribute_exists` on the key);\n * decrements carry the floor guard so a `-1` against an absent / `0`\n * counter is a no-op. Returns `true` when the delta landed, `false`\n * when the floor guard clamped a decrement or the canonical record was\n * missing.\n */\nexport async function applyCounterDeltaOperation(\n params: ApplyCounterDeltaParams,\n): Promise<boolean> {\n const { mutation, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const patch = buildPatch(service, mutation);\n\n try {\n if (mutation.delta > 0) {\n // Unconditional atomic ADD. `patch` adds `attribute_exists(PK)`,\n // so a delete-before-create race is clamped rather than\n // resurrecting the record.\n await patch.add({ [mutation.attribute]: mutation.delta }).go();\n return true;\n }\n\n // Floor-guarded decrement: apply `-1` only when the counter exists\n // and is strictly positive.\n await patch\n .add({ [mutation.attribute]: mutation.delta })\n .where((attr, { gt }) => gt(attr[mutation.attribute], 0))\n .go();\n return true;\n } catch (err) {\n if (isConditionalCheckFailure(err)) {\n return false;\n }\n throw err;\n }\n}\n\n/** Select the entity + composite key for the mutation's target. */\nfunction buildPatch(\n service: DynamoControlServiceType,\n mutation: CounterMutation,\n): PatchBuilder {\n switch (mutation.target) {\n case COUNTER_TARGET.Tenant:\n return service.entities.tenant.patch({\n tenantId: mutation.tenantId,\n sk: \"CURRENT\",\n }) as unknown as PatchBuilder;\n case COUNTER_TARGET.Workspace:\n return service.entities.workspace.patch({\n tenantId: mutation.tenantId,\n id: mutation.workspaceId,\n sk: \"CURRENT\",\n }) as unknown as PatchBuilder;\n case COUNTER_TARGET.User:\n return service.entities.user.patch({\n id: mutation.userId,\n sk: \"CURRENT\",\n }) as unknown as PatchBuilder;\n }\n}\n\n/**\n * ElectroDB wraps a failed conditional check in its own error. Detect it\n * structurally so the floor guard / existence guard resolves to a\n * clamped no-op rather than propagating.\n */\nfunction isConditionalCheckFailure(err: unknown): boolean {\n if (typeof err !== \"object\" || err === null) {\n return false;\n }\n const e = err as {\n name?: string;\n code?: string;\n message?: string;\n cause?: { name?: string };\n };\n if (\n e.name === \"ConditionalCheckFailedException\" ||\n e.code === \"ConditionalCheckFailedException\" ||\n e.cause?.name === \"ConditionalCheckFailedException\"\n ) {\n return true;\n }\n return typeof e.message === \"string\"\n ? e.message.includes(\"ConditionalCheckFailed\")\n : false;\n}\n","/**\n * ADR-028 admin-vs-normal classification for the workspace user breakdown.\n *\n * The counter-maintenance consumer counts every workspace-scoped\n * RoleAssignment into one of two buckets — `adminUsersInWorkspace` or\n * `normalUsersInWorkspace` — based on whether the assignment's role is\n * an admin-tier role. This module is the single documented predicate\n * that decision flows through so the rule lives in exactly one place.\n *\n * The signals come straight off the `control-plane.role-assignment-*`\n * event payload (no extra Role read): the ADR-019 organization-role\n * code carried as `roleLevel` (extracted from `PractitionerRole.code`\n * at publish time) and the `roleId` reference slug.\n *\n * Classification rule (per the #1318 brief against the ADR-019\n * vocabulary): an assignment is **admin** when either\n *\n * - its `roleLevel` code names an admin tier — it equals or ends with\n * `admin` (catches the data-plane organization role `billing-admin`\n * and the platform roles `tenant-admin` / `system-admin` when those\n * surface on the code), or\n * - its `roleId` matches `*admin*` (a defensive fallback for assignments\n * whose role code did not ride the event but whose stable role id\n * encodes the tier, e.g. `role-tenant-admin`).\n *\n * Everything else (including a missing roleLevel and roleId) classifies\n * as **normal**. Misclassification is self-correcting: ADR-028's\n * reconciliation job recomputes both buckets from canonical data and is\n * the authority on the true value.\n */\n\n/**\n * Returns `true` when a workspace-scoped RoleAssignment should count\n * toward `adminUsersInWorkspace`, `false` when it counts toward\n * `normalUsersInWorkspace`. See the module doc for the rule.\n */\nexport function isAdminRoleAssignment(input: {\n readonly roleLevel?: string;\n readonly roleId?: string;\n}): boolean {\n if (codeIsAdminTier(input.roleLevel)) {\n return true;\n }\n if (idMatchesAdmin(input.roleId)) {\n return true;\n }\n return false;\n}\n\n/**\n * An ADR-019 role code is admin-tier when it is exactly `admin` or ends\n * with the `-admin` / `admin` suffix (case-insensitive). Matches\n * `billing-admin`, `tenant-admin`, `system-admin`; rejects `biller`,\n * `scribe`, `practitioner`, etc.\n */\nfunction codeIsAdminTier(roleLevel: string | undefined): boolean {\n if (typeof roleLevel !== \"string\" || roleLevel.length === 0) {\n return false;\n }\n const lower = roleLevel.toLowerCase();\n return lower === \"admin\" || lower.endsWith(\"admin\");\n}\n\n/**\n * Fallback signal — the stable role id slug encodes the tier even when\n * the role code did not ride the event. Matches any id containing the\n * substring `admin` (case-insensitive), e.g. `role-tenant-admin`.\n */\nfunction idMatchesAdmin(roleId: string | undefined): boolean {\n if (typeof roleId !== \"string\" || roleId.length === 0) {\n return false;\n }\n return roleId.toLowerCase().includes(\"admin\");\n}\n"],"mappings":";;;;;AA8CO,IAAM,iBAAiB;AAAA,EAC5B,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,MAAM;AACR;AAyDA,eAAsB,2BACpB,QACkB;AAClB,QAAM,EAAE,UAAU,UAAU,IAAI;AAChC,QAAM,UAAU,wBAAwB,SAAS;AAEjD,QAAM,QAAQ,WAAW,SAAS,QAAQ;AAE1C,MAAI;AACF,QAAI,SAAS,QAAQ,GAAG;AAItB,YAAM,MAAM,IAAI,EAAE,CAAC,SAAS,SAAS,GAAG,SAAS,MAAM,CAAC,EAAE,GAAG;AAC7D,aAAO;AAAA,IACT;AAIA,UAAM,MACH,IAAI,EAAE,CAAC,SAAS,SAAS,GAAG,SAAS,MAAM,CAAC,EAC5C,MAAM,CAAC,MAAM,EAAE,GAAG,MAAM,GAAG,KAAK,SAAS,SAAS,GAAG,CAAC,CAAC,EACvD,GAAG;AACN,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,QAAI,0BAA0B,GAAG,GAAG;AAClC,aAAO;AAAA,IACT;AACA,UAAM;AAAA,EACR;AACF;AAGA,SAAS,WACP,SACA,UACc;AACd,UAAQ,SAAS,QAAQ;AAAA,IACvB,KAAK,eAAe;AAClB,aAAO,QAAQ,SAAS,OAAO,MAAM;AAAA,QACnC,UAAU,SAAS;AAAA,QACnB,IAAI;AAAA,MACN,CAAC;AAAA,IACH,KAAK,eAAe;AAClB,aAAO,QAAQ,SAAS,UAAU,MAAM;AAAA,QACtC,UAAU,SAAS;AAAA,QACnB,IAAI,SAAS;AAAA,QACb,IAAI;AAAA,MACN,CAAC;AAAA,IACH,KAAK,eAAe;AAClB,aAAO,QAAQ,SAAS,KAAK,MAAM;AAAA,QACjC,IAAI,SAAS;AAAA,QACb,IAAI;AAAA,MACN,CAAC;AAAA,EACL;AACF;AAOA,SAAS,0BAA0B,KAAuB;AACxD,MAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM;AAC3C,WAAO;AAAA,EACT;AACA,QAAM,IAAI;AAMV,MACE,EAAE,SAAS,qCACX,EAAE,SAAS,qCACX,EAAE,OAAO,SAAS,mCAClB;AACA,WAAO;AAAA,EACT;AACA,SAAO,OAAO,EAAE,YAAY,WACxB,EAAE,QAAQ,SAAS,wBAAwB,IAC3C;AACN;;;ACzJO,SAAS,sBAAsB,OAG1B;AACV,MAAI,gBAAgB,MAAM,SAAS,GAAG;AACpC,WAAO;AAAA,EACT;AACA,MAAI,eAAe,MAAM,MAAM,GAAG;AAChC,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAQA,SAAS,gBAAgB,WAAwC;AAC/D,MAAI,OAAO,cAAc,YAAY,UAAU,WAAW,GAAG;AAC3D,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,UAAU,YAAY;AACpC,SAAO,UAAU,WAAW,MAAM,SAAS,OAAO;AACpD;AAOA,SAAS,eAAe,QAAqC;AAC3D,MAAI,OAAO,WAAW,YAAY,OAAO,WAAW,GAAG;AACrD,WAAO;AAAA,EACT;AACA,SAAO,OAAO,YAAY,EAAE,SAAS,OAAO;AAC9C;","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getDynamoControlService
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-EUIP2U5F.mjs";
|
|
4
4
|
|
|
5
5
|
// src/data/operations/control/role/role-create-operation.ts
|
|
6
6
|
import { extractSummary } from "@openhi/types";
|
|
@@ -30,4 +30,4 @@ async function createRoleOperation(params) {
|
|
|
30
30
|
export {
|
|
31
31
|
createRoleOperation
|
|
32
32
|
};
|
|
33
|
-
//# sourceMappingURL=chunk-
|
|
33
|
+
//# sourceMappingURL=chunk-SD7J3N3C.mjs.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
require_lib
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-KMEWULMX.mjs";
|
|
4
4
|
import {
|
|
5
5
|
__toESM
|
|
6
6
|
} from "./chunk-LZOMFHX3.mjs";
|
|
@@ -19,4 +19,4 @@ export {
|
|
|
19
19
|
OWNING_DELETE_OPS_EVENT_BUS_ENV_VAR,
|
|
20
20
|
import_workflows
|
|
21
21
|
};
|
|
22
|
-
//# sourceMappingURL=chunk-
|
|
22
|
+
//# sourceMappingURL=chunk-VESULYQQ.mjs.map
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from "./chunk-MVQWAIMC.mjs";
|
|
2
|
+
membershipListByUserOperation
|
|
3
|
+
} from "./chunk-DWSWCUZR.mjs";
|
|
5
4
|
import {
|
|
6
5
|
ForbiddenError,
|
|
7
6
|
NotFoundError,
|
|
8
7
|
ValidationError
|
|
9
8
|
} from "./chunk-FYHBHHWK.mjs";
|
|
10
9
|
import {
|
|
11
|
-
SHARD_COUNT,
|
|
12
10
|
getDynamoControlService
|
|
13
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-EUIP2U5F.mjs";
|
|
14
12
|
|
|
15
13
|
// src/data/operations/control/user/user-find-by-sub-operation.ts
|
|
16
14
|
async function findUserBySubOperation(params) {
|
|
@@ -95,102 +93,9 @@ async function getUserByIdOperation(params) {
|
|
|
95
93
|
};
|
|
96
94
|
}
|
|
97
95
|
|
|
98
|
-
// src/data/operations/control/user/user-list-operation.ts
|
|
99
|
-
var SK = "CURRENT";
|
|
100
|
-
async function listUsersOperation(params) {
|
|
101
|
-
const { tableName, mode = "full" } = params;
|
|
102
|
-
const service = getDynamoControlService(tableName);
|
|
103
|
-
const shardResults = await Promise.all(
|
|
104
|
-
Array.from(
|
|
105
|
-
{ length: SHARD_COUNT },
|
|
106
|
-
(_, shard) => service.entities.user.query.gsi1({ gsi1Shard: String(shard) }).go()
|
|
107
|
-
)
|
|
108
|
-
);
|
|
109
|
-
return dispatchListMode(mode, shardResults, {
|
|
110
|
-
hydrate: (orderedIds) => batchGetWithRetry(
|
|
111
|
-
service.entities.user,
|
|
112
|
-
orderedIds.map((id) => ({ id, sk: SK }))
|
|
113
|
-
),
|
|
114
|
-
getId: (item) => item.id,
|
|
115
|
-
buildEntry: (id, item) => ({
|
|
116
|
-
id,
|
|
117
|
-
resource: {
|
|
118
|
-
resourceType: "User",
|
|
119
|
-
id,
|
|
120
|
-
...JSON.parse(item.resource)
|
|
121
|
-
}
|
|
122
|
-
}),
|
|
123
|
-
buildSummaryEntry: (id, parsed) => ({
|
|
124
|
-
id,
|
|
125
|
-
resource: { resourceType: "User", id, ...parsed }
|
|
126
|
-
})
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
96
|
// src/data/operations/control/user/user-switch-tenant-workspace-operation.ts
|
|
131
97
|
import { extractSummary as extractSummary2 } from "@openhi/types";
|
|
132
|
-
|
|
133
|
-
// src/data/operations/control/membership/membership-list-by-user-operation.ts
|
|
134
|
-
function buildSkPrefix(mode, tenantId) {
|
|
135
|
-
switch (mode) {
|
|
136
|
-
case "tenant":
|
|
137
|
-
return "MEMBERSHIP#TENANT#";
|
|
138
|
-
case "workspace":
|
|
139
|
-
return "MEMBERSHIP#WORKSPACE#";
|
|
140
|
-
case "workspaceInTenant":
|
|
141
|
-
return `MEMBERSHIP#WORKSPACE#TID#${tenantId}#`;
|
|
142
|
-
case "all":
|
|
143
|
-
default:
|
|
144
|
-
return "MEMBERSHIP#";
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
async function membershipListByUserOperation(params) {
|
|
148
|
-
const {
|
|
149
|
-
userId,
|
|
150
|
-
mode = "all",
|
|
151
|
-
tenantId,
|
|
152
|
-
cursor = null,
|
|
153
|
-
limit,
|
|
154
|
-
order,
|
|
155
|
-
tableName
|
|
156
|
-
} = params;
|
|
157
|
-
if (mode === "workspaceInTenant" && !tenantId) {
|
|
158
|
-
throw new Error(
|
|
159
|
-
'membershipListByUserOperation: tenantId is required when mode === "workspaceInTenant"'
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
const service = getDynamoControlService(tableName);
|
|
163
|
-
const skPrefix = buildSkPrefix(mode, tenantId);
|
|
164
|
-
const goOptions = {
|
|
165
|
-
cursor
|
|
166
|
-
};
|
|
167
|
-
if (limit !== void 0) {
|
|
168
|
-
goOptions.limit = limit;
|
|
169
|
-
}
|
|
170
|
-
if (order !== void 0) {
|
|
171
|
-
goOptions.order = order;
|
|
172
|
-
}
|
|
173
|
-
const result = await service.entities.membershipUserProjection.query.record({ userId }).begins({ sk: skPrefix }).go(goOptions);
|
|
174
|
-
const items = (result.data ?? []).map(
|
|
175
|
-
(row) => ({
|
|
176
|
-
userId: row.userId,
|
|
177
|
-
sk: row.sk,
|
|
178
|
-
tenantId: row.tenantId,
|
|
179
|
-
workspaceId: row.workspaceId,
|
|
180
|
-
membershipId: row.membershipId,
|
|
181
|
-
summary: row.summary,
|
|
182
|
-
vid: row.vid,
|
|
183
|
-
lastUpdated: row.lastUpdated,
|
|
184
|
-
denormalizedTenantName: row.denormalizedTenantName,
|
|
185
|
-
denormalizedUserName: row.denormalizedUserName,
|
|
186
|
-
denormalizedWorkspaceName: row.denormalizedWorkspaceName
|
|
187
|
-
})
|
|
188
|
-
);
|
|
189
|
-
return { items, cursor: result.cursor ?? null };
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// src/data/operations/control/user/user-switch-tenant-workspace-operation.ts
|
|
193
|
-
var SK2 = "CURRENT";
|
|
98
|
+
var SK = "CURRENT";
|
|
194
99
|
async function switchUserTenantWorkspaceOperation(params) {
|
|
195
100
|
const { cognitoSub, tenantReference, workspaceReference, tableName } = params;
|
|
196
101
|
const tenantId = idFromReference(tenantReference, "Tenant/");
|
|
@@ -251,7 +156,7 @@ async function switchUserTenantWorkspaceOperation(params) {
|
|
|
251
156
|
extractSummary2(updatedResource)
|
|
252
157
|
);
|
|
253
158
|
const service = getDynamoControlService(tableName);
|
|
254
|
-
await service.entities.user.patch({ id: user.id, sk:
|
|
159
|
+
await service.entities.user.patch({ id: user.id, sk: SK }).set({
|
|
255
160
|
resource: JSON.stringify(updatedResource),
|
|
256
161
|
summary,
|
|
257
162
|
vid,
|
|
@@ -303,12 +208,10 @@ export {
|
|
|
303
208
|
createUserOperation,
|
|
304
209
|
deleteUserOperation,
|
|
305
210
|
getUserByIdOperation,
|
|
306
|
-
membershipListByUserOperation,
|
|
307
|
-
listUsersOperation,
|
|
308
211
|
updateUserOperation,
|
|
309
212
|
findUserBySubOperation,
|
|
310
213
|
parseUserResource,
|
|
311
214
|
idFromReference,
|
|
312
215
|
switchUserTenantWorkspaceOperation
|
|
313
216
|
};
|
|
314
|
-
//# sourceMappingURL=chunk-
|
|
217
|
+
//# sourceMappingURL=chunk-WOTU36P3.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/data/operations/control/user/user-find-by-sub-operation.ts","../src/data/operations/control/user/user-resource-helpers.ts","../src/data/operations/fhir-reference.ts","../src/data/operations/control/user/user-create-operation.ts","../src/data/operations/control/user/user-get-by-id-operation.ts","../src/data/operations/control/user/user-switch-tenant-workspace-operation.ts","../src/data/operations/control/user/user-update-operation.ts","../src/data/operations/control/user/user-delete-operation.ts"],"sourcesContent":["import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { OpenHiContext } from \"../../../openhi-context\";\n\nexport interface FindUserBySubParams {\n context: OpenHiContext;\n cognitoSub: string;\n tableName?: string;\n}\n\nexport interface FindUserBySubResult {\n id: string;\n cognitoSub?: string;\n resource: string;\n summary?: string;\n vid: string;\n lastUpdated?: string;\n}\n\n/**\n * Look up a User by Cognito sub via GSI2, then fetch the canonical row\n * from the base table so projection-skipped attributes (`resource`,\n * `summary`, `vid`, `lastUpdated`) are populated. Returns `undefined`\n * when no row matches the sub, or when the GSI2 hit points at a\n * canonical row that has since been hard-deleted.\n *\n * GSI2 projects only `id` (+ ElectroDB markers), so a single index\n * query is insufficient — see #1175.\n */\nexport async function findUserBySubOperation(\n params: FindUserBySubParams,\n): Promise<FindUserBySubResult | undefined> {\n const { cognitoSub, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const indexResult = await service.entities.user.query\n .gsi2({ cognitoSub })\n .go({ limit: 1, attributes: [\"id\"] });\n const indexItem = indexResult.data?.[0];\n if (!indexItem) {\n return undefined;\n }\n\n const canonical = await service.entities.user\n .get({ id: indexItem.id, sk: \"CURRENT\" })\n .go();\n const row = canonical.data;\n if (!row) {\n return undefined;\n }\n\n return {\n id: row.id,\n cognitoSub: row.cognitoSub,\n resource: row.resource,\n summary: row.summary,\n vid: row.vid,\n lastUpdated: row.lastUpdated,\n };\n}\n","import type { User } from \"@openhi/types\";\n\n/**\n * Helpers for working with persisted OpenHI User resources. Co-located with\n * the User operations because both the Cognito triggers and the onboarding\n * workflow consume these alongside `findUserBySubOperation`.\n */\n\n// Defensive parse — JSON.parse may yield any shape, so every field is optional.\nexport type UserResource = Partial<User>;\n\n/**\n * Existing User resources are stored as JSON strings in the data store; parse\n * defensively so a malformed payload returns `undefined` rather than throwing.\n */\nexport function parseUserResource(resource: string): UserResource | undefined {\n try {\n return JSON.parse(resource) as UserResource;\n } catch {\n return undefined;\n }\n}\n","/**\n * Pure helpers for working with FHIR Reference fields. Shared across data-plane\n * and control-plane operations and the handlers that wrap them.\n */\n\n/**\n * Extract the id portion from a FHIR-style reference such as `Patient/<id>` or\n * `Tenant/<id>`. Returns `undefined` if the reference is missing, does not\n * match the prefix, or has an empty id after the prefix.\n */\nexport function idFromReference(\n reference: string | undefined,\n prefix: string,\n): string | undefined {\n if (!reference || !reference.startsWith(prefix)) {\n return undefined;\n }\n const id = reference.slice(prefix.length);\n return id.length > 0 ? id : undefined;\n}\n","import { extractSummary, type FhirResourceLike } from \"@openhi/types\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { OpenHiContext } from \"../../../openhi-context\";\n\nexport interface UserCreateParams {\n context: OpenHiContext;\n body: { id?: string; resource?: Record<string, unknown> | string };\n tableName?: string;\n}\n\nexport interface UserCreateResult {\n id: string;\n resource: { resourceType: string; id: string; [key: string]: unknown };\n meta: { lastUpdated: string; versionId: string };\n}\n\nexport async function createUserOperation(\n params: UserCreateParams,\n): Promise<UserCreateResult> {\n const { context, body, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const id = body.id ?? `user-${Date.now()}`;\n const parsedResource =\n typeof body.resource === \"string\"\n ? (JSON.parse(body.resource) as Record<string, unknown>)\n : (body.resource ?? {});\n\n const lastUpdated = context.date ?? new Date().toISOString();\n const vid = `1`;\n\n const resource = { resourceType: \"User\", id, ...parsedResource };\n const summary = JSON.stringify(extractSummary(resource as FhirResourceLike));\n\n await service.entities.user\n .put({\n id,\n resource: JSON.stringify(resource),\n summary,\n vid,\n lastUpdated,\n })\n .go();\n\n return {\n id,\n resource,\n meta: { lastUpdated, versionId: vid },\n };\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { NotFoundError } from \"../../../errors\";\nimport { OpenHiContext } from \"../../../openhi-context\";\n\nexport interface UserGetByIdParams {\n context: OpenHiContext;\n id: string;\n tableName?: string;\n}\n\nexport interface UserGetByIdResult {\n id: string;\n resource: { resourceType: string; id: string; [key: string]: unknown };\n}\n\nexport async function getUserByIdOperation(\n params: UserGetByIdParams,\n): Promise<UserGetByIdResult> {\n const { id, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const response = await service.entities.user.get({ id, sk: \"CURRENT\" }).go();\n\n const item = response.data;\n if (!item) {\n throw new NotFoundError(`User not found: ${id}`);\n }\n\n const parsedResource = JSON.parse(item.resource) as Record<string, unknown>;\n\n return {\n id,\n resource: { resourceType: \"User\", id, ...parsedResource },\n };\n}\n","import { extractSummary, type FhirResourceLike } from \"@openhi/types\";\nimport { findUserBySubOperation } from \"./user-find-by-sub-operation\";\nimport { parseUserResource } from \"./user-resource-helpers\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport {\n ForbiddenError,\n NotFoundError,\n ValidationError,\n} from \"../../../errors\";\nimport { idFromReference } from \"../../fhir-reference\";\nimport { membershipListByUserOperation } from \"../membership/membership-list-by-user-operation\";\n\nconst SK = \"CURRENT\";\n\nexport interface UserSwitchTenantWorkspaceParams {\n cognitoSub: string;\n tenantReference: string;\n workspaceReference: string;\n tableName?: string;\n /** Override the clock — used by tests for deterministic `lastUpdated`. */\n now?: () => Date;\n}\n\nexport interface UserSwitchTenantWorkspaceResult {\n id: string;\n resource: Record<string, unknown>;\n meta: { lastUpdated: string; versionId: string };\n}\n\n/**\n * Update `currentTenant` and `currentWorkspace` on the User resource for the\n * caller authenticated by the given Cognito `sub`. All other fields on the\n * User are preserved.\n *\n * Membership pre-condition uses the ADR-018 adjacency-list user projection\n * (pattern #4, workspace sub-lane). A single base-table Query on\n * `PK = USER#ID#<userId>` with\n * `SK begins_with 'MEMBERSHIP#WORKSPACE#TID#<tenantId>#'`\n * (via {@link membershipListByUserOperation} with `mode: \"workspaceInTenant\"`)\n * confirms the caller has a workspace-level Membership in the requested\n * tenant + workspace pair. No GSI1 fan-out, no scan.\n *\n * Throws:\n * - `ValidationError` when either reference is missing or malformed\n * - `NotFoundError` when no User matches the Cognito subject\n * - `ForbiddenError` when the caller has no Membership in the requested\n * `(tenantId, workspaceId)` pair on their user-partition projection\n *\n * @see https://github.com/codedrifters/openhi/issues/769\n * @see https://github.com/codedrifters/openhi/issues/1020\n * @see ADR-018 § Access Pattern Coverage (pattern #4)\n */\nexport async function switchUserTenantWorkspaceOperation(\n params: UserSwitchTenantWorkspaceParams,\n): Promise<UserSwitchTenantWorkspaceResult> {\n const { cognitoSub, tenantReference, workspaceReference, tableName } = params;\n\n const tenantId = idFromReference(tenantReference, \"Tenant/\");\n if (!tenantId) {\n throw new ValidationError(\n \"tenant.reference must be a 'Tenant/<id>' reference.\",\n );\n }\n const workspaceId = idFromReference(workspaceReference, \"Workspace/\");\n if (!workspaceId) {\n throw new ValidationError(\n \"workspace.reference must be a 'Workspace/<id>' reference.\",\n );\n }\n\n const user = await findUserBySubOperation({\n // findUserBySubOperation does not read context fields; pass a stub.\n context: {\n tenantId: \"\",\n workspaceId: \"\",\n date: \"\",\n actorId: \"\",\n actorName: \"\",\n actorType: \"internal-system\",\n },\n cognitoSub,\n tableName,\n });\n if (!user) {\n throw new NotFoundError(\n \"User not yet provisioned for the authenticated Cognito subject.\",\n );\n }\n\n // ADR-018: single Query on the user partition, narrowed to the workspace\n // sub-lane of the requested tenant. The lane includes `workspaceId` on\n // every row; a row with `workspaceId === <requested>` is sufficient and\n // necessary proof that the caller may switch to that pair.\n const projection = await membershipListByUserOperation({\n userId: user.id,\n mode: \"workspaceInTenant\",\n tenantId,\n tableName,\n });\n const hasMembership = projection.items.some(\n (row) => row.workspaceId === workspaceId,\n );\n if (!hasMembership) {\n throw new ForbiddenError(\n `User is not a member of Workspace/${workspaceId} in Tenant/${tenantId}.`,\n );\n }\n\n const existingResource = parseUserResource(user.resource) ?? {};\n const updatedResource: Record<string, unknown> = {\n ...existingResource,\n resourceType: \"User\",\n id: user.id,\n currentTenant: { reference: `Tenant/${tenantId}` },\n currentWorkspace: { reference: `Workspace/${workspaceId}` },\n };\n\n const lastUpdated = (params.now ? params.now() : new Date()).toISOString();\n const vid = `${Date.now()}`;\n const summary = JSON.stringify(\n extractSummary(updatedResource as FhirResourceLike),\n );\n\n const service = getDynamoControlService(tableName);\n await service.entities.user\n .patch({ id: user.id, sk: SK })\n .set({\n resource: JSON.stringify(updatedResource),\n summary,\n vid,\n lastUpdated,\n })\n .go();\n\n return {\n id: user.id,\n resource: updatedResource,\n meta: { lastUpdated, versionId: vid },\n };\n}\n","import { extractSummary, type FhirResourceLike } from \"@openhi/types\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { NotFoundError } from \"../../../errors\";\nimport { OpenHiContext } from \"../../../openhi-context\";\n\nexport interface UserUpdateParams {\n context: OpenHiContext;\n id: string;\n body: { resource?: Record<string, unknown> | string };\n tableName?: string;\n}\n\nexport interface UserUpdateResult {\n id: string;\n resource: { resourceType: string; id: string; [key: string]: unknown };\n meta: { lastUpdated: string; versionId: string };\n}\n\nexport async function updateUserOperation(\n params: UserUpdateParams,\n): Promise<UserUpdateResult> {\n const { context, id, body, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const existing = await service.entities.user.get({ id, sk: \"CURRENT\" }).go();\n if (!existing.data) {\n throw new NotFoundError(`User not found: ${id}`);\n }\n\n const parsedResource =\n typeof body.resource === \"string\"\n ? (JSON.parse(body.resource) as Record<string, unknown>)\n : (body.resource ?? {});\n\n const lastUpdated = context.date ?? new Date().toISOString();\n const vid = `${Date.now()}`;\n\n const resource = { resourceType: \"User\", id, ...parsedResource };\n const summary = JSON.stringify(extractSummary(resource as FhirResourceLike));\n\n await service.entities.user\n .put({\n id,\n resource: JSON.stringify(resource),\n summary,\n vid,\n lastUpdated,\n })\n .go();\n\n return {\n id,\n resource,\n meta: { lastUpdated, versionId: vid },\n };\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { OpenHiContext } from \"../../../openhi-context\";\n\nexport interface UserDeleteParams {\n context: OpenHiContext;\n id: string;\n tableName?: string;\n}\n\nexport async function deleteUserOperation(\n params: UserDeleteParams,\n): Promise<void> {\n const { id, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n await service.entities.user.delete({ id, sk: \"CURRENT\" }).go();\n}\n"],"mappings":"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","names":["extractSummary","extractSummary","extractSummary","extractSummary"]}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-QJDHVMKT.mjs";
|
|
5
5
|
import {
|
|
6
6
|
getDynamoControlService
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-EUIP2U5F.mjs";
|
|
8
8
|
|
|
9
9
|
// src/data/operations/control/rename-cascade/rename-cascade-rewrite-chunk-operation.ts
|
|
10
10
|
var RENAME_CASCADE_MAX_TARGETS_PER_CHUNK = 50;
|
|
@@ -63,4 +63,4 @@ export {
|
|
|
63
63
|
rewriteRenameCascadeChunkOperation,
|
|
64
64
|
chunkRenameCascadeTargets
|
|
65
65
|
};
|
|
66
|
-
//# sourceMappingURL=chunk-
|
|
66
|
+
//# sourceMappingURL=chunk-YPTJJ35S.mjs.map
|