@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.
Files changed (111) hide show
  1. package/lib/chunk-23PUSHBV.mjs +24 -0
  2. package/lib/chunk-23PUSHBV.mjs.map +1 -0
  3. package/lib/{chunk-7FUAMZOF.mjs → chunk-53OHXLIL.mjs} +3 -3
  4. package/lib/chunk-6NBGYGFL.mjs +1803 -0
  5. package/lib/chunk-6NBGYGFL.mjs.map +1 -0
  6. package/lib/chunk-7RZHFI77.mjs +22 -0
  7. package/lib/chunk-7RZHFI77.mjs.map +1 -0
  8. package/lib/{chunk-7Q2IJ2J5.mjs → chunk-CUUKXDB2.mjs} +6 -6
  9. package/lib/chunk-FYHBHHWK.mjs +47 -0
  10. package/lib/chunk-FYHBHHWK.mjs.map +1 -0
  11. package/lib/{chunk-MULKGFIJ.mjs → chunk-GBDIGTNV.mjs} +165 -10
  12. package/lib/chunk-GBDIGTNV.mjs.map +1 -0
  13. package/lib/chunk-HQ67J7BP.mjs +199 -0
  14. package/lib/chunk-HQ67J7BP.mjs.map +1 -0
  15. package/lib/{chunk-AJ3G3THO.mjs → chunk-KO64HPWQ.mjs} +2 -2
  16. package/lib/{chunk-BB5MK4L3.mjs → chunk-KSFC72TT.mjs} +3 -3
  17. package/lib/{chunk-2TPJ6HOF.mjs → chunk-NZRW7ROK.mjs} +72 -54
  18. package/lib/chunk-NZRW7ROK.mjs.map +1 -0
  19. package/lib/chunk-QJDHVMKT.mjs +117 -0
  20. package/lib/chunk-QJDHVMKT.mjs.map +1 -0
  21. package/lib/{chunk-IS4VQRI4.mjs → chunk-QMBJ4VHC.mjs} +12 -47
  22. package/lib/chunk-QMBJ4VHC.mjs.map +1 -0
  23. package/lib/chunk-TRY7JGWO.mjs +16 -0
  24. package/lib/chunk-TRY7JGWO.mjs.map +1 -0
  25. package/lib/chunk-W4KR4CSL.mjs +236 -0
  26. package/lib/chunk-W4KR4CSL.mjs.map +1 -0
  27. package/lib/{chunk-AGF3RAAZ.mjs → chunk-WPCBVDFZ.mjs} +2 -2
  28. package/lib/chunk-WQWFVEVX.mjs +66 -0
  29. package/lib/chunk-WQWFVEVX.mjs.map +1 -0
  30. package/lib/{chunk-SYBADQXI.mjs → chunk-ZM4GDHHC.mjs} +77 -2
  31. package/lib/chunk-ZM4GDHHC.mjs.map +1 -0
  32. package/lib/delete-chunk.handler.d.mts +29 -0
  33. package/lib/delete-chunk.handler.d.ts +29 -0
  34. package/lib/delete-chunk.handler.js +2716 -0
  35. package/lib/delete-chunk.handler.js.map +1 -0
  36. package/lib/delete-chunk.handler.mjs +47 -0
  37. package/lib/delete-chunk.handler.mjs.map +1 -0
  38. package/lib/events-CjS-sm0W.d.mts +107 -0
  39. package/lib/events-CjS-sm0W.d.ts +107 -0
  40. package/lib/events-Da_cFgtc.d.mts +208 -0
  41. package/lib/events-Da_cFgtc.d.ts +208 -0
  42. package/lib/finalize.handler.d.mts +35 -0
  43. package/lib/finalize.handler.d.ts +35 -0
  44. package/lib/finalize.handler.js +875 -0
  45. package/lib/finalize.handler.js.map +1 -0
  46. package/lib/finalize.handler.mjs +166 -0
  47. package/lib/finalize.handler.mjs.map +1 -0
  48. package/lib/index.d.mts +189 -2
  49. package/lib/index.d.ts +500 -3
  50. package/lib/index.js +1753 -174
  51. package/lib/index.js.map +1 -1
  52. package/lib/index.mjs +571 -17
  53. package/lib/index.mjs.map +1 -1
  54. package/lib/list-chunks.handler.d.mts +28 -0
  55. package/lib/list-chunks.handler.d.ts +28 -0
  56. package/lib/list-chunks.handler.js +2746 -0
  57. package/lib/list-chunks.handler.js.map +1 -0
  58. package/lib/list-chunks.handler.mjs +54 -0
  59. package/lib/list-chunks.handler.mjs.map +1 -0
  60. package/lib/platform-deploy-bridge.handler.js +76 -1
  61. package/lib/platform-deploy-bridge.handler.js.map +1 -1
  62. package/lib/platform-deploy-bridge.handler.mjs +1 -1
  63. package/lib/pre-token-generation.handler.js +1106 -155
  64. package/lib/pre-token-generation.handler.js.map +1 -1
  65. package/lib/pre-token-generation.handler.mjs +6 -4
  66. package/lib/pre-token-generation.handler.mjs.map +1 -1
  67. package/lib/provision-default-workspace.handler.js +1529 -142
  68. package/lib/provision-default-workspace.handler.js.map +1 -1
  69. package/lib/provision-default-workspace.handler.mjs +8 -4
  70. package/lib/provision-default-workspace.handler.mjs.map +1 -1
  71. package/lib/rename-finalize.handler.d.mts +30 -0
  72. package/lib/rename-finalize.handler.d.ts +30 -0
  73. package/lib/rename-finalize.handler.js +795 -0
  74. package/lib/rename-finalize.handler.js.map +1 -0
  75. package/lib/rename-finalize.handler.mjs +90 -0
  76. package/lib/rename-finalize.handler.mjs.map +1 -0
  77. package/lib/rename-list-targets.handler.d.mts +26 -0
  78. package/lib/rename-list-targets.handler.d.ts +26 -0
  79. package/lib/rename-list-targets.handler.js +2985 -0
  80. package/lib/rename-list-targets.handler.js.map +1 -0
  81. package/lib/rename-list-targets.handler.mjs +431 -0
  82. package/lib/rename-list-targets.handler.mjs.map +1 -0
  83. package/lib/rename-rewrite-chunk.handler.d.mts +35 -0
  84. package/lib/rename-rewrite-chunk.handler.d.ts +35 -0
  85. package/lib/rename-rewrite-chunk.handler.js +2021 -0
  86. package/lib/rename-rewrite-chunk.handler.js.map +1 -0
  87. package/lib/rename-rewrite-chunk.handler.mjs +27 -0
  88. package/lib/rename-rewrite-chunk.handler.mjs.map +1 -0
  89. package/lib/rest-api-lambda.handler.js +4021 -932
  90. package/lib/rest-api-lambda.handler.js.map +1 -1
  91. package/lib/rest-api-lambda.handler.mjs +1786 -80
  92. package/lib/rest-api-lambda.handler.mjs.map +1 -1
  93. package/lib/seed-demo-data.handler.js +1588 -124
  94. package/lib/seed-demo-data.handler.js.map +1 -1
  95. package/lib/seed-demo-data.handler.mjs +10 -6
  96. package/lib/seed-system-data.handler.js +1179 -155
  97. package/lib/seed-system-data.handler.js.map +1 -1
  98. package/lib/seed-system-data.handler.mjs +5 -4
  99. package/lib/seed-system-data.handler.mjs.map +1 -1
  100. package/package.json +2 -2
  101. package/lib/chunk-2TPJ6HOF.mjs.map +0 -1
  102. package/lib/chunk-IS4VQRI4.mjs.map +0 -1
  103. package/lib/chunk-MULKGFIJ.mjs.map +0 -1
  104. package/lib/chunk-QR5JVSCF.mjs +0 -862
  105. package/lib/chunk-QR5JVSCF.mjs.map +0 -1
  106. package/lib/chunk-SYBADQXI.mjs.map +0 -1
  107. /package/lib/{chunk-7FUAMZOF.mjs.map → chunk-53OHXLIL.mjs.map} +0 -0
  108. /package/lib/{chunk-7Q2IJ2J5.mjs.map → chunk-CUUKXDB2.mjs.map} +0 -0
  109. /package/lib/{chunk-AJ3G3THO.mjs.map → chunk-KO64HPWQ.mjs.map} +0 -0
  110. /package/lib/{chunk-BB5MK4L3.mjs.map → chunk-KSFC72TT.mjs.map} +0 -0
  111. /package/lib/{chunk-AGF3RAAZ.mjs.map → chunk-WPCBVDFZ.mjs.map} +0 -0
@@ -0,0 +1,1803 @@
1
+ import {
2
+ defaultTableName,
3
+ dynamoClient
4
+ } from "./chunk-TRY7JGWO.mjs";
5
+
6
+ // src/data/dynamo/dynamo-control-service.ts
7
+ import { Service } from "electrodb";
8
+
9
+ // src/data/dynamo/entities/control/configuration-entity.ts
10
+ import { Entity } from "electrodb";
11
+
12
+ // src/data/dynamo/entities/control/control-entity-common.ts
13
+ import { extractLabel, normalizeLabel } from "@openhi/types";
14
+
15
+ // src/data/dynamo/shard.ts
16
+ var SHARD_COUNT = 4;
17
+ function computeShard(id) {
18
+ let hash = 2166136261;
19
+ for (let i = 0; i < id.length; i++) {
20
+ hash ^= id.charCodeAt(i);
21
+ hash = Math.imul(hash, 16777619);
22
+ }
23
+ return (hash >>> 0) % SHARD_COUNT;
24
+ }
25
+
26
+ // src/data/dynamo/entities/control/control-entity-common.ts
27
+ var gsi1ShardAttribute = {
28
+ type: "string",
29
+ watch: ["id"],
30
+ set: (_val, item) => {
31
+ if (typeof item?.id !== "string" || item.id.length === 0) {
32
+ return void 0;
33
+ }
34
+ return String(computeShard(item.id));
35
+ }
36
+ };
37
+ var gsi1skAttribute = {
38
+ type: "string",
39
+ watch: ["resource", "lastUpdated", "id"],
40
+ set: (_val, item) => {
41
+ const id = typeof item?.id === "string" ? item.id : "";
42
+ const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
43
+ const fallback = `${lastUpdated}#${id}`;
44
+ if (typeof item?.resource !== "string" || item.resource.length === 0) {
45
+ return fallback;
46
+ }
47
+ let parsed;
48
+ try {
49
+ parsed = JSON.parse(item.resource);
50
+ } catch {
51
+ return fallback;
52
+ }
53
+ if (!parsed || typeof parsed !== "object") return fallback;
54
+ const resourceType = parsed.resourceType;
55
+ if (typeof resourceType !== "string") return fallback;
56
+ const label = extractLabel(parsed);
57
+ return label !== void 0 ? `${label}#${id}` : fallback;
58
+ }
59
+ };
60
+ function extractRoleId(resource) {
61
+ const flat = resource.roleId;
62
+ if (typeof flat === "string" && flat.length > 0) return flat;
63
+ const role = resource.role;
64
+ if (role && typeof role === "object") {
65
+ const reference = role.reference;
66
+ if (typeof reference === "string" && reference.length > 0) {
67
+ const slash = reference.lastIndexOf("/");
68
+ const tail = slash >= 0 ? reference.slice(slash + 1) : reference;
69
+ if (tail.length > 0) return tail;
70
+ }
71
+ }
72
+ return void 0;
73
+ }
74
+ var roleAssignmentGsi1skAttribute = {
75
+ type: "string",
76
+ watch: ["resource", "denormalizedUserName", "lastUpdated", "id"],
77
+ set: (_val, item) => {
78
+ const id = typeof item?.id === "string" ? item.id : "";
79
+ const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
80
+ const fallback = `${lastUpdated}#${id}`;
81
+ if (typeof item?.resource !== "string" || item.resource.length === 0) {
82
+ return fallback;
83
+ }
84
+ let parsed;
85
+ try {
86
+ parsed = JSON.parse(item.resource);
87
+ } catch {
88
+ return fallback;
89
+ }
90
+ if (!parsed || typeof parsed !== "object") return fallback;
91
+ const roleId = extractRoleId(parsed);
92
+ if (roleId === void 0) return fallback;
93
+ const denormalizedUserName = typeof item.denormalizedUserName === "string" ? item.denormalizedUserName : "";
94
+ const normalizedUserName = denormalizedUserName.length > 0 ? normalizeLabel(denormalizedUserName) : "";
95
+ if (normalizedUserName.length === 0) return fallback;
96
+ return `${roleId}#${normalizedUserName}#${id}`;
97
+ }
98
+ };
99
+ var membershipGsi1skAttribute = {
100
+ type: "string",
101
+ watch: ["denormalizedUserName", "lastUpdated", "id"],
102
+ set: (_val, item) => {
103
+ const id = typeof item?.id === "string" ? item.id : "";
104
+ const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
105
+ const fallback = `${lastUpdated}#${id}`;
106
+ const denormalizedUserName = typeof item?.denormalizedUserName === "string" ? item.denormalizedUserName : "";
107
+ const normalizedUserName = denormalizedUserName.length > 0 ? normalizeLabel(denormalizedUserName) : "";
108
+ if (normalizedUserName.length === 0) {
109
+ return fallback;
110
+ }
111
+ return `${normalizedUserName}#${id}`;
112
+ }
113
+ };
114
+
115
+ // src/data/dynamo/entities/control/configuration-entity.ts
116
+ var ConfigurationEntity = new Entity({
117
+ model: {
118
+ entity: "configuration",
119
+ service: "control",
120
+ version: "01"
121
+ },
122
+ attributes: {
123
+ /** Sort key. "CURRENT" for current version; version history in S3. */
124
+ sk: {
125
+ type: "string",
126
+ required: true,
127
+ default: "CURRENT"
128
+ },
129
+ /** Tenant scope. Use "BASELINE" when the config is baseline default (no tenant). */
130
+ tenantId: {
131
+ type: "string",
132
+ required: true,
133
+ default: "BASELINE"
134
+ },
135
+ /** Workspace scope. Use "-" when absent. */
136
+ workspaceId: {
137
+ type: "string",
138
+ required: true,
139
+ default: "-"
140
+ },
141
+ /** User scope. Use "-" when absent. */
142
+ userId: {
143
+ type: "string",
144
+ required: true,
145
+ default: "-"
146
+ },
147
+ /** Role scope. Use "-" when absent. */
148
+ roleId: {
149
+ type: "string",
150
+ required: true,
151
+ default: "-"
152
+ },
153
+ /** Config type (category), e.g. endpoints, branding, display. */
154
+ key: {
155
+ type: "string",
156
+ required: true
157
+ },
158
+ /** FHIR Resource.id; logical id in URL and for the Configuration resource. */
159
+ id: {
160
+ type: "string",
161
+ required: true
162
+ },
163
+ /** Payload as JSON string. JSON.stringify(resource) on write; JSON.parse(item.resource) on read. */
164
+ resource: {
165
+ type: "string",
166
+ required: true
167
+ },
168
+ /**
169
+ * Summary projection (key display fields as JSON string: id, key, status).
170
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
171
+ */
172
+ summary: {
173
+ type: "string",
174
+ required: true
175
+ },
176
+ /** Version id (e.g. ULID). Tracks current version; S3 history key. */
177
+ vid: {
178
+ type: "string",
179
+ required: true
180
+ },
181
+ lastUpdated: {
182
+ type: "string",
183
+ required: true
184
+ },
185
+ gsi1Shard: gsi1ShardAttribute,
186
+ deleted: {
187
+ type: "boolean",
188
+ required: false
189
+ },
190
+ bundleId: {
191
+ type: "string",
192
+ required: false
193
+ },
194
+ msgId: {
195
+ type: "string",
196
+ required: false
197
+ }
198
+ },
199
+ indexes: {
200
+ /** Base table: PK, SK (data store key names). PK is built from tenantId, workspaceId, userId, roleId; SK is built from key and sk. Do not supply PK or SK from outside. */
201
+ record: {
202
+ pk: {
203
+ field: "PK",
204
+ composite: ["tenantId", "workspaceId", "userId", "roleId"],
205
+ template: "CONFIG#TID#${tenantId}#WID#${workspaceId}#UID#${userId}#RID#${roleId}"
206
+ },
207
+ sk: {
208
+ field: "SK",
209
+ composite: ["key", "sk"],
210
+ template: "KEY#${key}#SK#${sk}"
211
+ }
212
+ },
213
+ /**
214
+ * GSI1 — Unified Sharded List per ADR-011: list all Configuration entries for a
215
+ * (tenant, workspace) across the four shards. Use for "list configs scoped to this tenant"
216
+ * (workspaceId = "-") or "list configs scoped to this workspace". Does not support
217
+ * hierarchical resolution in one query; use base table GetItem in fallback order
218
+ * (user → workspace → tenant → baseline) for that.
219
+ * SK is `<key>#<id>` — Configuration's `key` is a required entity attribute (the
220
+ * config category: endpoints, branding, display, …) and the natural sort/lookup
221
+ * dimension. `casing: "none"` preserves the literal key value.
222
+ */
223
+ gsi1: {
224
+ index: "GSI1",
225
+ pk: {
226
+ field: "GSI1PK",
227
+ composite: ["tenantId", "workspaceId", "gsi1Shard"],
228
+ template: "TID#${tenantId}#WID#${workspaceId}#RT#Configuration#SHARD#${gsi1Shard}"
229
+ },
230
+ sk: {
231
+ field: "GSI1SK",
232
+ casing: "none",
233
+ composite: ["key", "id"],
234
+ template: "${key}#${id}"
235
+ }
236
+ }
237
+ }
238
+ });
239
+
240
+ // src/data/dynamo/entities/control/configuration-user-projection-entity.ts
241
+ import { Entity as Entity2 } from "electrodb";
242
+ var ConfigurationUserProjectionEntity = new Entity2({
243
+ model: {
244
+ entity: "configurationUserProjection",
245
+ service: "control",
246
+ version: "01"
247
+ },
248
+ attributes: {
249
+ /**
250
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
251
+ * base-table PK. Always required — the projection has no meaning
252
+ * outside a user partition.
253
+ */
254
+ userId: {
255
+ type: "string",
256
+ required: true
257
+ },
258
+ /**
259
+ * Pre-composed sort key — built by the operations-layer projection
260
+ * writer via `buildConfigurationUserProjectionSk`. The entity stores
261
+ * the value verbatim so the SK grammar (pattern #10 user-scope) is
262
+ * owned by the operations layer, not duplicated here.
263
+ */
264
+ sk: {
265
+ type: "string",
266
+ required: true
267
+ },
268
+ /**
269
+ * Configuration canonical-record id. Stored as a discriminating
270
+ * field so consumers can hydrate the canonical row via the
271
+ * Configuration get-by-id operation when the projection's `summary`
272
+ * is insufficient.
273
+ */
274
+ configurationId: {
275
+ type: "string",
276
+ required: true
277
+ },
278
+ /**
279
+ * Tenant the Configuration is associated with. The canonical row
280
+ * keys off `(tenantId, workspaceId, userId, roleId)`; the projection
281
+ * carries `tenantId` so consumers reconstructing the canonical PK
282
+ * have the tenant segment without a hop.
283
+ */
284
+ tenantId: {
285
+ type: "string",
286
+ required: true
287
+ },
288
+ /**
289
+ * Scope marker. Always `"user"` on this projection — recorded
290
+ * explicitly so future scope-bearing projections (workspace,
291
+ * tenant, role) can share filter semantics in a unified
292
+ * cross-projection list query if one ever lands.
293
+ */
294
+ scope: {
295
+ type: "string",
296
+ required: true,
297
+ default: "user"
298
+ },
299
+ /**
300
+ * Configuration's `key` attribute (config category, e.g. endpoints,
301
+ * branding, display). Mirrored from the canonical row so consumers
302
+ * reading the projection get the natural display label without a
303
+ * BatchGet hop. Doubles as the source of `<normalizedConfigName>` in
304
+ * the SK.
305
+ */
306
+ displayName: {
307
+ type: "string",
308
+ required: false
309
+ },
310
+ /**
311
+ * Summary projection (key display fields as JSON string) — mirrored
312
+ * from the canonical Configuration row so user-partition queries do
313
+ * not need a BatchGet hop.
314
+ */
315
+ summary: {
316
+ type: "string",
317
+ required: true
318
+ },
319
+ /** Version id mirrored from the canonical Configuration row. */
320
+ vid: {
321
+ type: "string",
322
+ required: true
323
+ },
324
+ /** Last-updated timestamp mirrored from the canonical Configuration row. */
325
+ lastUpdated: {
326
+ type: "string",
327
+ required: true
328
+ }
329
+ },
330
+ indexes: {
331
+ /**
332
+ * Base table: PK = USER#ID#<userId>, SK = operation-supplied. A
333
+ * single `Query(PK = USER#ID#<userId>, SK begins_with
334
+ * 'CONFIGURATION#')` returns the user's user-scoped Configurations
335
+ * sorted by `<normalizedConfigName>` (then `<configurationId>` as
336
+ * the tiebreaker).
337
+ */
338
+ record: {
339
+ pk: {
340
+ field: "PK",
341
+ composite: ["userId"],
342
+ template: "USER#ID#${userId}"
343
+ },
344
+ sk: {
345
+ field: "SK",
346
+ casing: "none",
347
+ composite: ["sk"],
348
+ template: "${sk}"
349
+ }
350
+ }
351
+ }
352
+ });
353
+
354
+ // src/data/dynamo/entities/control/configuration-workspace-projection-entity.ts
355
+ import { Entity as Entity3 } from "electrodb";
356
+ var ConfigurationWorkspaceProjectionEntity = new Entity3({
357
+ model: {
358
+ entity: "configurationWorkspaceProjection",
359
+ service: "control",
360
+ version: "01"
361
+ },
362
+ attributes: {
363
+ /**
364
+ * Tenant the workspace belongs to. Renders as the leading segment
365
+ * of the base-table PK. Always required — the workspace partition
366
+ * is tenant-scoped per ADR-011.
367
+ */
368
+ tenantId: {
369
+ type: "string",
370
+ required: true
371
+ },
372
+ /**
373
+ * Workspace partition discriminator. Renders as the trailing
374
+ * segment of the base-table PK
375
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
376
+ * the projection has no meaning outside a workspace partition.
377
+ */
378
+ workspaceId: {
379
+ type: "string",
380
+ required: true
381
+ },
382
+ /**
383
+ * Pre-composed sort key — built by the operations-layer projection
384
+ * writer via `buildConfigurationWorkspaceProjectionSk`. The entity
385
+ * stores the value verbatim so the SK grammar (pattern #10
386
+ * workspace-scope) is owned by the operations layer, not
387
+ * duplicated here.
388
+ */
389
+ sk: {
390
+ type: "string",
391
+ required: true
392
+ },
393
+ /**
394
+ * Configuration canonical-record id. Stored as a discriminating
395
+ * field so consumers can hydrate the canonical row via the
396
+ * Configuration get-by-id operation when the projection's `summary`
397
+ * is insufficient.
398
+ */
399
+ configurationId: {
400
+ type: "string",
401
+ required: true
402
+ },
403
+ /**
404
+ * Scope marker. Always `"workspace"` on this projection — recorded
405
+ * explicitly so future scope-bearing projections (user, tenant,
406
+ * role) can share filter semantics in a unified cross-projection
407
+ * list query if one ever lands.
408
+ */
409
+ scope: {
410
+ type: "string",
411
+ required: true,
412
+ default: "workspace"
413
+ },
414
+ /**
415
+ * Configuration's `key` attribute (config category, e.g. endpoints,
416
+ * branding, display). Mirrored from the canonical row so consumers
417
+ * reading the projection get the natural display label without a
418
+ * BatchGet hop. Doubles as the source of `<normalizedConfigName>`
419
+ * in the SK.
420
+ */
421
+ displayName: {
422
+ type: "string",
423
+ required: false
424
+ },
425
+ /**
426
+ * Summary projection (key display fields as JSON string) — mirrored
427
+ * from the canonical Configuration row so workspace-partition
428
+ * queries do not need a BatchGet hop.
429
+ */
430
+ summary: {
431
+ type: "string",
432
+ required: true
433
+ },
434
+ /** Version id mirrored from the canonical Configuration row. */
435
+ vid: {
436
+ type: "string",
437
+ required: true
438
+ },
439
+ /** Last-updated timestamp mirrored from the canonical Configuration row. */
440
+ lastUpdated: {
441
+ type: "string",
442
+ required: true
443
+ }
444
+ },
445
+ indexes: {
446
+ /**
447
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
448
+ * SK = operation-supplied. A single
449
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'CONFIGURATION#')`
450
+ * returns the workspace's workspace-scoped Configurations sorted by
451
+ * `<normalizedConfigName>` (then `<configurationId>` as the
452
+ * tiebreaker).
453
+ */
454
+ record: {
455
+ pk: {
456
+ field: "PK",
457
+ composite: ["tenantId", "workspaceId"],
458
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
459
+ },
460
+ sk: {
461
+ field: "SK",
462
+ casing: "none",
463
+ composite: ["sk"],
464
+ template: "${sk}"
465
+ }
466
+ }
467
+ }
468
+ });
469
+
470
+ // src/data/dynamo/entities/control/membership-entity.ts
471
+ import { Entity as Entity4 } from "electrodb";
472
+ var MembershipEntity = new Entity4({
473
+ model: {
474
+ entity: "membership",
475
+ service: "control",
476
+ version: "01"
477
+ },
478
+ attributes: {
479
+ /** Sort key sentinel. Always "CURRENT". */
480
+ sk: {
481
+ type: "string",
482
+ required: true,
483
+ default: "CURRENT"
484
+ },
485
+ /** Tenant in which the user has membership (required). */
486
+ tenantId: {
487
+ type: "string",
488
+ required: true
489
+ },
490
+ /** FHIR Resource.id; membership id. */
491
+ id: {
492
+ type: "string",
493
+ required: true
494
+ },
495
+ /** Full Membership resource serialized as JSON string. */
496
+ resource: {
497
+ type: "string",
498
+ required: true
499
+ },
500
+ /**
501
+ * Summary projection (key display fields as JSON string: id, displayName, status).
502
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
503
+ */
504
+ summary: {
505
+ type: "string",
506
+ required: true
507
+ },
508
+ /** Version id (e.g. ULID). */
509
+ vid: {
510
+ type: "string",
511
+ required: true
512
+ },
513
+ lastUpdated: {
514
+ type: "string",
515
+ required: true
516
+ },
517
+ gsi1Shard: gsi1ShardAttribute,
518
+ /**
519
+ * Derived GSI1 sort key — `<normalizedUserName>#<id>` per ADR-018
520
+ * pattern #1 so a GSI1 query partitioned on the tenant range-scans
521
+ * by user-name prefix and returns memberships sorted by user name.
522
+ * Falls back to `<lastUpdated>#<id>` when `denormalizedUserName`
523
+ * is missing.
524
+ */
525
+ gsi1sk: membershipGsi1skAttribute,
526
+ deleted: {
527
+ type: "boolean",
528
+ required: false
529
+ },
530
+ bundleId: {
531
+ type: "string",
532
+ required: false
533
+ },
534
+ msgId: {
535
+ type: "string",
536
+ required: false
537
+ },
538
+ /**
539
+ * Denormalized `linked-data-identity` Reference (e.g. `Practitioner/abc`).
540
+ * Populated from the FHIR extension on the Membership resource at write
541
+ * time so future GSIs can index data-plane identity lookups without
542
+ * deserializing the full resource JSON. See ADR 2026-03-13-02 §6.
543
+ */
544
+ linkedDataIdentityRef: {
545
+ type: "string",
546
+ required: false
547
+ },
548
+ /**
549
+ * Denormalized display name of the linked Tenant, captured at row
550
+ * last-write time. Promoted to a top-level attribute so the ADR-018
551
+ * adjacency-list projection SKs (pattern #3 — `MEMBERSHIP#TENANT#<normalizedTenantName>#…`)
552
+ * can be composed from a top-level field instead of digging into the
553
+ * `resource` JSON. Optional on the schema so pre-TR-024 rows do not
554
+ * break; the operations-layer multi-write helper (#1010) makes the
555
+ * field load-bearing at write time per TR-024 rule 2 (write-time
556
+ * source = canonical Tenant.displayName).
557
+ * @see TR-024 — Denormalized display-name attributes
558
+ */
559
+ denormalizedTenantName: {
560
+ type: "string",
561
+ required: false
562
+ },
563
+ /**
564
+ * Denormalized display name of the linked User, captured at row
565
+ * last-write time. Promoted to a top-level attribute so the ADR-018
566
+ * adjacency-list canonical-record GSI1SK (pattern #1 —
567
+ * `<normalizedUserName>#<id>`) and workspace-projection SK (pattern #2)
568
+ * can be composed from a top-level field. Optional on the schema so
569
+ * pre-TR-024 rows do not break; the operations-layer multi-write helper
570
+ * (#1010) makes the field load-bearing at write time per TR-024 rule 2
571
+ * (write-time source = canonical User.displayName).
572
+ * @see TR-024 — Denormalized display-name attributes
573
+ */
574
+ denormalizedUserName: {
575
+ type: "string",
576
+ required: false
577
+ }
578
+ },
579
+ indexes: {
580
+ /** Base table: PK = TID#<tenantId>#MEMBERSHIP#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
581
+ record: {
582
+ pk: {
583
+ field: "PK",
584
+ composite: ["tenantId", "id"],
585
+ template: "TID#${tenantId}#MEMBERSHIP#ID#${id}"
586
+ },
587
+ sk: {
588
+ field: "SK",
589
+ composite: ["sk"],
590
+ template: "${sk}"
591
+ }
592
+ },
593
+ /**
594
+ * GSI1 — Unified Sharded List per ADR-011: list all Memberships for a tenant across the
595
+ * four shards. Membership is tenant-scoped only, so `WID#-` is a sentinel.
596
+ * SK is derived via `membershipGsi1skAttribute` — composes
597
+ * `<normalizedUserName>#<id>` per ADR-018 pattern #1 (users in a
598
+ * tenant, sorted by user name); falls back to `<lastUpdated>#<id>`
599
+ * when `denormalizedUserName` is missing. `casing: "none"` preserves
600
+ * the normalized label and ISO-8601 `T`/`Z`.
601
+ */
602
+ gsi1: {
603
+ index: "GSI1",
604
+ pk: {
605
+ field: "GSI1PK",
606
+ composite: ["tenantId", "gsi1Shard"],
607
+ template: "TID#${tenantId}#WID#-#RT#Membership#SHARD#${gsi1Shard}"
608
+ },
609
+ sk: {
610
+ field: "GSI1SK",
611
+ casing: "none",
612
+ composite: ["gsi1sk"],
613
+ template: "${gsi1sk}"
614
+ }
615
+ }
616
+ }
617
+ });
618
+
619
+ // src/data/dynamo/entities/control/membership-user-projection-entity.ts
620
+ import { Entity as Entity5 } from "electrodb";
621
+ var MembershipUserProjectionEntity = new Entity5({
622
+ model: {
623
+ entity: "membershipUserProjection",
624
+ service: "control",
625
+ version: "01"
626
+ },
627
+ attributes: {
628
+ /**
629
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
630
+ * base-table PK. Always required — the projection has no meaning
631
+ * outside a user partition.
632
+ */
633
+ userId: {
634
+ type: "string",
635
+ required: true
636
+ },
637
+ /**
638
+ * Pre-composed sort key — built by the operations-layer projection
639
+ * writer via `buildMembershipUserProjectionSk*` helpers. The entity
640
+ * stores the value verbatim so the SK grammar (patterns #3 and #4)
641
+ * is owned by the operations layer, not duplicated here.
642
+ */
643
+ sk: {
644
+ type: "string",
645
+ required: true
646
+ },
647
+ /** Tenant in which the membership applies. Always required. */
648
+ tenantId: {
649
+ type: "string",
650
+ required: true
651
+ },
652
+ /**
653
+ * Workspace the membership scopes to. Present iff the projection
654
+ * row is a pattern-#4 workspace sub-lane row; absent for pattern-#3
655
+ * tenant sub-lane rows.
656
+ */
657
+ workspaceId: {
658
+ type: "string",
659
+ required: false
660
+ },
661
+ /**
662
+ * Membership canonical-record id. Stored as a discriminating field
663
+ * so consumers can hydrate the canonical row via
664
+ * `MembershipEntity.get({ tenantId, id: membershipId })` when the
665
+ * projection's `summary` is insufficient.
666
+ */
667
+ membershipId: {
668
+ type: "string",
669
+ required: true
670
+ },
671
+ /**
672
+ * Summary projection (key display fields as JSON string: id,
673
+ * displayName, status) — mirrored from the canonical Membership row
674
+ * so user-partition queries do not need a BatchGet hop.
675
+ */
676
+ summary: {
677
+ type: "string",
678
+ required: true
679
+ },
680
+ /** Version id mirrored from the canonical Membership row. */
681
+ vid: {
682
+ type: "string",
683
+ required: true
684
+ },
685
+ /** Last-updated timestamp mirrored from the canonical Membership row. */
686
+ lastUpdated: {
687
+ type: "string",
688
+ required: true
689
+ },
690
+ /**
691
+ * Denormalized Tenant display name — required to compose pattern-#3
692
+ * SK (`MEMBERSHIP#TENANT#<normalizedTenantName>#…`). Optional on the
693
+ * schema because pre-TR-024 rows may not carry a display name; the
694
+ * operations layer falls back gracefully when missing.
695
+ */
696
+ denormalizedTenantName: {
697
+ type: "string",
698
+ required: false
699
+ },
700
+ /**
701
+ * Denormalized User display name — mirrored from the canonical
702
+ * Membership row per TR-024 rule 3 (canonical-record symmetry).
703
+ * Carried on the projection so consumers can render the user's
704
+ * display name without a hop to the User record.
705
+ */
706
+ denormalizedUserName: {
707
+ type: "string",
708
+ required: false
709
+ },
710
+ /**
711
+ * Denormalized Workspace display name — required to compose
712
+ * pattern-#4 SK (`MEMBERSHIP#WORKSPACE#TID#<tenantId>#<normalizedWorkspaceName>#…`).
713
+ * Optional on the schema (TR-024 § Open Item #4 defers a formal
714
+ * Workspace-rename cascade); the operations layer falls back to a
715
+ * sentinel when missing so the SK still has a valid shape.
716
+ */
717
+ denormalizedWorkspaceName: {
718
+ type: "string",
719
+ required: false
720
+ }
721
+ },
722
+ indexes: {
723
+ /**
724
+ * Base table: PK = USER#ID#<userId>, SK = operation-supplied.
725
+ * Both pattern #3 and pattern #4 use this same index — the SK string
726
+ * encodes the lane discriminator (`MEMBERSHIP#TENANT#…` vs
727
+ * `MEMBERSHIP#WORKSPACE#…`) so a single `Query(PK = USER#ID#<userId>,
728
+ * SK begins_with 'MEMBERSHIP#')` returns both lanes interleaved.
729
+ */
730
+ record: {
731
+ pk: {
732
+ field: "PK",
733
+ composite: ["userId"],
734
+ template: "USER#ID#${userId}"
735
+ },
736
+ sk: {
737
+ field: "SK",
738
+ casing: "none",
739
+ composite: ["sk"],
740
+ template: "${sk}"
741
+ }
742
+ }
743
+ }
744
+ });
745
+
746
+ // src/data/dynamo/entities/control/membership-workspace-projection-entity.ts
747
+ import { Entity as Entity6 } from "electrodb";
748
+ var MembershipWorkspaceProjectionEntity = new Entity6({
749
+ model: {
750
+ entity: "membershipWorkspaceProjection",
751
+ service: "control",
752
+ version: "01"
753
+ },
754
+ attributes: {
755
+ /**
756
+ * Tenant the workspace belongs to. Renders as the leading segment
757
+ * of the base-table PK. Always required — the workspace partition
758
+ * is tenant-scoped per ADR-011.
759
+ */
760
+ tenantId: {
761
+ type: "string",
762
+ required: true
763
+ },
764
+ /**
765
+ * Workspace partition discriminator. Renders as the trailing
766
+ * segment of the base-table PK
767
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
768
+ * the projection has no meaning outside a workspace partition.
769
+ */
770
+ workspaceId: {
771
+ type: "string",
772
+ required: true
773
+ },
774
+ /**
775
+ * Pre-composed sort key — built by the operations-layer projection
776
+ * writer via `buildMembershipWorkspaceProjectionSk`. The entity
777
+ * stores the value verbatim so the SK grammar (pattern #2) is
778
+ * owned by the operations layer, not duplicated here.
779
+ */
780
+ sk: {
781
+ type: "string",
782
+ required: true
783
+ },
784
+ /**
785
+ * User the membership links. Stored as a discriminating field so
786
+ * consumers can hydrate the canonical User row via
787
+ * `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
788
+ * projection's `summary` is insufficient.
789
+ */
790
+ userId: {
791
+ type: "string",
792
+ required: true
793
+ },
794
+ /**
795
+ * Membership canonical-record id. Stored as a discriminating field
796
+ * so consumers can hydrate the canonical row via
797
+ * `MembershipEntity.get({ tenantId, id: membershipId })` when the
798
+ * projection's `summary` is insufficient.
799
+ */
800
+ membershipId: {
801
+ type: "string",
802
+ required: true
803
+ },
804
+ /**
805
+ * Summary projection (key display fields as JSON string: id,
806
+ * displayName, status) — mirrored from the canonical Membership row
807
+ * so workspace-partition queries do not need a BatchGet hop.
808
+ */
809
+ summary: {
810
+ type: "string",
811
+ required: true
812
+ },
813
+ /** Version id mirrored from the canonical Membership row. */
814
+ vid: {
815
+ type: "string",
816
+ required: true
817
+ },
818
+ /** Last-updated timestamp mirrored from the canonical Membership row. */
819
+ lastUpdated: {
820
+ type: "string",
821
+ required: true
822
+ },
823
+ /**
824
+ * Denormalized User display name — required to compose the
825
+ * pattern-#2 SK (`MEMBERSHIP#<normalizedUserName>#…`). Optional on
826
+ * the schema because pre-TR-024 rows may not carry a display name;
827
+ * the operations layer falls back to a sentinel when missing so
828
+ * the SK still has a valid shape. The TR-023 rename-cascade
829
+ * pipeline rewrites the SK on a User rename.
830
+ */
831
+ denormalizedUserName: {
832
+ type: "string",
833
+ required: false
834
+ }
835
+ },
836
+ indexes: {
837
+ /**
838
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
839
+ * SK = operation-supplied. Pattern #2 uses this index — the SK
840
+ * encodes the entity-type prefix (`MEMBERSHIP#…`) so a
841
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'MEMBERSHIP#')`
842
+ * returns every member projection for the workspace in normalized-
843
+ * user-name sort order.
844
+ */
845
+ record: {
846
+ pk: {
847
+ field: "PK",
848
+ composite: ["tenantId", "workspaceId"],
849
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
850
+ },
851
+ sk: {
852
+ field: "SK",
853
+ casing: "none",
854
+ composite: ["sk"],
855
+ template: "${sk}"
856
+ }
857
+ }
858
+ }
859
+ });
860
+
861
+ // src/data/dynamo/entities/control/role-entity.ts
862
+ import { Entity as Entity7 } from "electrodb";
863
+ var RoleEntity = new Entity7({
864
+ model: {
865
+ entity: "role",
866
+ service: "control",
867
+ version: "01"
868
+ },
869
+ attributes: {
870
+ /** Sort key sentinel. Always "CURRENT". */
871
+ sk: {
872
+ type: "string",
873
+ required: true,
874
+ default: "CURRENT"
875
+ },
876
+ /** FHIR Resource.id; role id. */
877
+ id: {
878
+ type: "string",
879
+ required: true
880
+ },
881
+ /** Full Role resource serialized as JSON string. */
882
+ resource: {
883
+ type: "string",
884
+ required: true
885
+ },
886
+ /**
887
+ * Summary projection (key display fields as JSON string: id, displayName, status).
888
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
889
+ */
890
+ summary: {
891
+ type: "string",
892
+ required: true
893
+ },
894
+ /** Version id (e.g. ULID). */
895
+ vid: {
896
+ type: "string",
897
+ required: true
898
+ },
899
+ lastUpdated: {
900
+ type: "string",
901
+ required: true
902
+ },
903
+ gsi1Shard: gsi1ShardAttribute,
904
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
905
+ gsi1sk: gsi1skAttribute,
906
+ deleted: {
907
+ type: "boolean",
908
+ required: false
909
+ },
910
+ bundleId: {
911
+ type: "string",
912
+ required: false
913
+ },
914
+ msgId: {
915
+ type: "string",
916
+ required: false
917
+ }
918
+ },
919
+ indexes: {
920
+ /** Base table: PK = ROLE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
921
+ record: {
922
+ pk: {
923
+ field: "PK",
924
+ composite: ["id"],
925
+ template: "ROLE#ID#${id}"
926
+ },
927
+ sk: {
928
+ field: "SK",
929
+ composite: ["sk"],
930
+ template: "${sk}"
931
+ }
932
+ },
933
+ /**
934
+ * GSI1 — Unified Sharded List per ADR-011: list all Roles across the four shards.
935
+ * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#Role#SHARD#<n>`.
936
+ * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
937
+ * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
938
+ * normalized label and ISO-8601 `T`/`Z`.
939
+ */
940
+ gsi1: {
941
+ index: "GSI1",
942
+ pk: {
943
+ field: "GSI1PK",
944
+ composite: ["gsi1Shard"],
945
+ template: "TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}"
946
+ },
947
+ sk: {
948
+ field: "GSI1SK",
949
+ casing: "none",
950
+ composite: ["gsi1sk"],
951
+ template: "${gsi1sk}"
952
+ }
953
+ }
954
+ }
955
+ });
956
+
957
+ // src/data/dynamo/entities/control/roleassignment-entity.ts
958
+ import { Entity as Entity8 } from "electrodb";
959
+ var RoleAssignmentEntity = new Entity8({
960
+ model: {
961
+ entity: "roleassignment",
962
+ service: "control",
963
+ version: "01"
964
+ },
965
+ attributes: {
966
+ /** Sort key sentinel. Always "CURRENT". */
967
+ sk: {
968
+ type: "string",
969
+ required: true,
970
+ default: "CURRENT"
971
+ },
972
+ /** Tenant in which the role assignment applies (required). */
973
+ tenantId: {
974
+ type: "string",
975
+ required: true
976
+ },
977
+ /** FHIR Resource.id; role assignment id. */
978
+ id: {
979
+ type: "string",
980
+ required: true
981
+ },
982
+ /** Full RoleAssignment resource serialized as JSON string. */
983
+ resource: {
984
+ type: "string",
985
+ required: true
986
+ },
987
+ /**
988
+ * Summary projection (key display fields as JSON string: id, displayName, status).
989
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
990
+ */
991
+ summary: {
992
+ type: "string",
993
+ required: true
994
+ },
995
+ /** Version id (e.g. ULID). */
996
+ vid: {
997
+ type: "string",
998
+ required: true
999
+ },
1000
+ lastUpdated: {
1001
+ type: "string",
1002
+ required: true
1003
+ },
1004
+ gsi1Shard: gsi1ShardAttribute,
1005
+ /**
1006
+ * Derived GSI1 sort key — discriminator-first
1007
+ * `<roleId>#<normalizedUserName>#<id>` per ADR-018 pattern #8 so a
1008
+ * GSI1 query partitioned on the tenant can `begins_with('<roleId>#')`
1009
+ * to enumerate every user assigned to a given role, sorted by user
1010
+ * name. Falls back to `<lastUpdated>#<id>` when either component is
1011
+ * missing.
1012
+ */
1013
+ gsi1sk: roleAssignmentGsi1skAttribute,
1014
+ deleted: {
1015
+ type: "boolean",
1016
+ required: false
1017
+ },
1018
+ bundleId: {
1019
+ type: "string",
1020
+ required: false
1021
+ },
1022
+ msgId: {
1023
+ type: "string",
1024
+ required: false
1025
+ },
1026
+ /**
1027
+ * Denormalized display name of the linked Tenant, captured at row
1028
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1029
+ * adjacency-list user-projection SK (pattern #5 —
1030
+ * `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#<roleId>#TID#<tenantId>#<id>`)
1031
+ * can be composed from a top-level field instead of digging into the
1032
+ * `resource` JSON. Optional on the schema so pre-TR-024 rows do not
1033
+ * break; the operations-layer multi-write helper (#1010) makes the
1034
+ * field load-bearing at write time per TR-024 rule 2 (write-time
1035
+ * source = canonical Tenant.displayName).
1036
+ * @see TR-024 — Denormalized display-name attributes
1037
+ */
1038
+ denormalizedTenantName: {
1039
+ type: "string",
1040
+ required: false
1041
+ },
1042
+ /**
1043
+ * Denormalized display name of the linked User, captured at row
1044
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1045
+ * adjacency-list canonical-record GSI1SK (pattern #8 —
1046
+ * `<roleId>#<normalizedUserName>#<id>`) and workspace-projection SK
1047
+ * (pattern #9) can be composed from a top-level field. Optional on
1048
+ * the schema so pre-TR-024 rows do not break; the operations-layer
1049
+ * multi-write helper (#1010) makes the field load-bearing at write
1050
+ * time per TR-024 rule 2 (write-time source = canonical
1051
+ * User.displayName).
1052
+ * @see TR-024 — Denormalized display-name attributes
1053
+ */
1054
+ denormalizedUserName: {
1055
+ type: "string",
1056
+ required: false
1057
+ },
1058
+ /**
1059
+ * Denormalized display name of the linked Role, captured at row
1060
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1061
+ * adjacency-list user-projection SK (pattern #5 —
1062
+ * `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#…`) can be composed from
1063
+ * a top-level field. Optional on the schema so pre-TR-024 rows do not
1064
+ * break; the operations-layer multi-write helper (#1010) makes the
1065
+ * field load-bearing at write time per TR-024 rule 2 (write-time
1066
+ * source = canonical Role.displayName).
1067
+ * @see TR-024 — Denormalized display-name attributes
1068
+ */
1069
+ denormalizedRoleName: {
1070
+ type: "string",
1071
+ required: false
1072
+ }
1073
+ },
1074
+ indexes: {
1075
+ /** Base table: PK = TID#<tenantId>#ROLEASSIGNMENT#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
1076
+ record: {
1077
+ pk: {
1078
+ field: "PK",
1079
+ composite: ["tenantId", "id"],
1080
+ template: "TID#${tenantId}#ROLEASSIGNMENT#ID#${id}"
1081
+ },
1082
+ sk: {
1083
+ field: "SK",
1084
+ composite: ["sk"],
1085
+ template: "${sk}"
1086
+ }
1087
+ },
1088
+ /**
1089
+ * GSI1 — Unified Sharded List per ADR-011: list all RoleAssignments for a tenant across the
1090
+ * four shards. Tenant-scoped only, so `WID#-` is a sentinel.
1091
+ * SK is derived via `roleAssignmentGsi1skAttribute` — composes the
1092
+ * discriminator-first `<roleId>#<normalizedUserName>#<id>` shape per
1093
+ * ADR-018 pattern #8 (users with a specific role in a tenant, sorted
1094
+ * by user name); falls back to `<lastUpdated>#<id>` when either
1095
+ * component is missing. `casing: "none"` preserves the normalized
1096
+ * label and ISO-8601 `T`/`Z`.
1097
+ */
1098
+ gsi1: {
1099
+ index: "GSI1",
1100
+ pk: {
1101
+ field: "GSI1PK",
1102
+ composite: ["tenantId", "gsi1Shard"],
1103
+ template: "TID#${tenantId}#WID#-#RT#RoleAssignment#SHARD#${gsi1Shard}"
1104
+ },
1105
+ sk: {
1106
+ field: "GSI1SK",
1107
+ casing: "none",
1108
+ composite: ["gsi1sk"],
1109
+ template: "${gsi1sk}"
1110
+ }
1111
+ }
1112
+ }
1113
+ });
1114
+
1115
+ // src/data/dynamo/entities/control/roleassignment-user-projection-entity.ts
1116
+ import { Entity as Entity9 } from "electrodb";
1117
+ var RoleAssignmentUserProjectionEntity = new Entity9({
1118
+ model: {
1119
+ entity: "roleAssignmentUserProjection",
1120
+ service: "control",
1121
+ version: "01"
1122
+ },
1123
+ attributes: {
1124
+ /**
1125
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
1126
+ * base-table PK. Always required — the projection has no meaning
1127
+ * outside a user partition.
1128
+ */
1129
+ userId: {
1130
+ type: "string",
1131
+ required: true
1132
+ },
1133
+ /**
1134
+ * Pre-composed sort key — built by the operations-layer projection
1135
+ * writer via `buildRoleAssignmentUserProjectionSk*` helpers. The
1136
+ * entity stores the value verbatim so the SK grammar (tenant-lane
1137
+ * vs workspace-lane) is owned by the operations layer, not
1138
+ * duplicated here.
1139
+ */
1140
+ sk: {
1141
+ type: "string",
1142
+ required: true
1143
+ },
1144
+ /** Tenant in which the role assignment applies. Always required. */
1145
+ tenantId: {
1146
+ type: "string",
1147
+ required: true
1148
+ },
1149
+ /**
1150
+ * Workspace the role assignment scopes to. Present iff the
1151
+ * projection row is the workspace-level sub-lane; absent for
1152
+ * tenant-level sub-lane rows.
1153
+ */
1154
+ workspaceId: {
1155
+ type: "string",
1156
+ required: false
1157
+ },
1158
+ /**
1159
+ * Role the assignment grants. Stored as a discriminating field so
1160
+ * `Query(PK = USER#ID#<userId>, SK begins_with 'ROLEASSIGNMENT#…')`
1161
+ * results carry the role id without a hop to the canonical row.
1162
+ */
1163
+ roleId: {
1164
+ type: "string",
1165
+ required: true
1166
+ },
1167
+ /**
1168
+ * RoleAssignment canonical-record id. Stored as a discriminating
1169
+ * field so consumers can hydrate the canonical row via
1170
+ * `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
1171
+ * when the projection's `summary` is insufficient.
1172
+ */
1173
+ roleAssignmentId: {
1174
+ type: "string",
1175
+ required: true
1176
+ },
1177
+ /**
1178
+ * Summary projection (key display fields as JSON string: id,
1179
+ * displayName, status) — mirrored from the canonical RoleAssignment
1180
+ * row so user-partition queries do not need a BatchGet hop.
1181
+ */
1182
+ summary: {
1183
+ type: "string",
1184
+ required: true
1185
+ },
1186
+ /** Version id mirrored from the canonical RoleAssignment row. */
1187
+ vid: {
1188
+ type: "string",
1189
+ required: true
1190
+ },
1191
+ /** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
1192
+ lastUpdated: {
1193
+ type: "string",
1194
+ required: true
1195
+ },
1196
+ /**
1197
+ * Denormalized Tenant display name — mirrored from the canonical
1198
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
1199
+ * Optional on the schema because pre-TR-024 rows may not carry a
1200
+ * display name; the operations layer falls back gracefully when
1201
+ * missing.
1202
+ */
1203
+ denormalizedTenantName: {
1204
+ type: "string",
1205
+ required: false
1206
+ },
1207
+ /**
1208
+ * Denormalized User display name — mirrored from the canonical
1209
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
1210
+ * Carried on the projection so consumers can render the user's
1211
+ * display name without a hop to the User record.
1212
+ */
1213
+ denormalizedUserName: {
1214
+ type: "string",
1215
+ required: false
1216
+ },
1217
+ /**
1218
+ * Denormalized Role display name — required to compose the SK's
1219
+ * `<normalizedRoleName>` segment. Optional on the schema (pre-TR-024
1220
+ * rows fall back to a sentinel) but expected to be present at write
1221
+ * time per TR-024 rule 2 (write-time source =
1222
+ * canonical Role.displayName).
1223
+ */
1224
+ denormalizedRoleName: {
1225
+ type: "string",
1226
+ required: false
1227
+ }
1228
+ },
1229
+ indexes: {
1230
+ /**
1231
+ * Base table: PK = USER#ID#<userId>, SK = operation-supplied. Both
1232
+ * sub-lanes (tenant-level and workspace-level) use this same index —
1233
+ * the SK string encodes the lane discriminator
1234
+ * (`ROLEASSIGNMENT#TENANT#…` vs `ROLEASSIGNMENT#WORKSPACE#…`) so a
1235
+ * single `Query(PK = USER#ID#<userId>, SK begins_with
1236
+ * 'ROLEASSIGNMENT#')` returns both lanes interleaved.
1237
+ */
1238
+ record: {
1239
+ pk: {
1240
+ field: "PK",
1241
+ composite: ["userId"],
1242
+ template: "USER#ID#${userId}"
1243
+ },
1244
+ sk: {
1245
+ field: "SK",
1246
+ casing: "none",
1247
+ composite: ["sk"],
1248
+ template: "${sk}"
1249
+ }
1250
+ }
1251
+ }
1252
+ });
1253
+
1254
+ // src/data/dynamo/entities/control/roleassignment-workspace-projection-entity.ts
1255
+ import { Entity as Entity10 } from "electrodb";
1256
+ var RoleAssignmentWorkspaceProjectionEntity = new Entity10({
1257
+ model: {
1258
+ entity: "roleAssignmentWorkspaceProjection",
1259
+ service: "control",
1260
+ version: "01"
1261
+ },
1262
+ attributes: {
1263
+ /**
1264
+ * Tenant the workspace belongs to. Renders as the leading segment
1265
+ * of the base-table PK. Always required — the workspace partition
1266
+ * is tenant-scoped per ADR-011.
1267
+ */
1268
+ tenantId: {
1269
+ type: "string",
1270
+ required: true
1271
+ },
1272
+ /**
1273
+ * Workspace partition discriminator. Renders as the trailing
1274
+ * segment of the base-table PK
1275
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
1276
+ * the projection has no meaning outside a workspace partition.
1277
+ */
1278
+ workspaceId: {
1279
+ type: "string",
1280
+ required: true
1281
+ },
1282
+ /**
1283
+ * Pre-composed sort key — built by the operations-layer projection
1284
+ * writer via `buildRoleAssignmentWorkspaceProjectionSk`. The entity
1285
+ * stores the value verbatim so the SK grammar (pattern #9) is
1286
+ * owned by the operations layer, not duplicated here.
1287
+ */
1288
+ sk: {
1289
+ type: "string",
1290
+ required: true
1291
+ },
1292
+ /**
1293
+ * User the role assignment grants the role to. Stored as a
1294
+ * discriminating field so consumers can hydrate the canonical User
1295
+ * row via `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
1296
+ * projection's `summary` is insufficient.
1297
+ */
1298
+ userId: {
1299
+ type: "string",
1300
+ required: true
1301
+ },
1302
+ /**
1303
+ * Role the assignment grants. Stored as a discriminating field —
1304
+ * also rendered into the SK as the discriminator-first segment so
1305
+ * `begins_with('ROLEASSIGNMENT#<roleId>#')` filters one role.
1306
+ */
1307
+ roleId: {
1308
+ type: "string",
1309
+ required: true
1310
+ },
1311
+ /**
1312
+ * RoleAssignment canonical-record id. Stored as a discriminating
1313
+ * field so consumers can hydrate the canonical row via
1314
+ * `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
1315
+ * when the projection's `summary` is insufficient.
1316
+ */
1317
+ roleAssignmentId: {
1318
+ type: "string",
1319
+ required: true
1320
+ },
1321
+ /**
1322
+ * Summary projection (key display fields as JSON string: id,
1323
+ * displayName, status) — mirrored from the canonical RoleAssignment
1324
+ * row so workspace-partition queries do not need a BatchGet hop.
1325
+ */
1326
+ summary: {
1327
+ type: "string",
1328
+ required: true
1329
+ },
1330
+ /** Version id mirrored from the canonical RoleAssignment row. */
1331
+ vid: {
1332
+ type: "string",
1333
+ required: true
1334
+ },
1335
+ /** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
1336
+ lastUpdated: {
1337
+ type: "string",
1338
+ required: true
1339
+ },
1340
+ /**
1341
+ * Denormalized User display name — required to compose the
1342
+ * pattern-#9 SK (`ROLEASSIGNMENT#<roleId>#<normalizedUserName>#…`).
1343
+ * Optional on the schema because pre-TR-024 rows may not carry a
1344
+ * display name; the operations layer falls back to a sentinel when
1345
+ * missing so the SK still has a valid shape. The TR-023 rename-
1346
+ * cascade pipeline rewrites the SK on a User rename.
1347
+ */
1348
+ denormalizedUserName: {
1349
+ type: "string",
1350
+ required: false
1351
+ },
1352
+ /**
1353
+ * Denormalized Role display name — mirrored from the canonical
1354
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
1355
+ * Carried on the projection so consumers can render the role's
1356
+ * display name without a hop to the Role record. Not part of the
1357
+ * SK (pattern #9 sorts on `<normalizedUserName>`, not role name) —
1358
+ * a Role rename does NOT rewrite this SK.
1359
+ */
1360
+ denormalizedRoleName: {
1361
+ type: "string",
1362
+ required: false
1363
+ }
1364
+ },
1365
+ indexes: {
1366
+ /**
1367
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
1368
+ * SK = operation-supplied. Pattern #9 uses this index — the SK
1369
+ * encodes the entity-type prefix and discriminator-first roleId
1370
+ * (`ROLEASSIGNMENT#<roleId>#…`) so
1371
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'ROLEASSIGNMENT#<roleId>#')`
1372
+ * returns every user-assignment for that role in the workspace, sorted
1373
+ * by normalized user name.
1374
+ */
1375
+ record: {
1376
+ pk: {
1377
+ field: "PK",
1378
+ composite: ["tenantId", "workspaceId"],
1379
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
1380
+ },
1381
+ sk: {
1382
+ field: "SK",
1383
+ casing: "none",
1384
+ composite: ["sk"],
1385
+ template: "${sk}"
1386
+ }
1387
+ }
1388
+ }
1389
+ });
1390
+
1391
+ // src/data/dynamo/entities/control/tenant-entity.ts
1392
+ import { Entity as Entity11 } from "electrodb";
1393
+ var TenantEntity = new Entity11({
1394
+ model: {
1395
+ entity: "tenant",
1396
+ service: "control",
1397
+ version: "01"
1398
+ },
1399
+ attributes: {
1400
+ /** Sort key sentinel. Always "CURRENT". */
1401
+ sk: {
1402
+ type: "string",
1403
+ required: true,
1404
+ default: "CURRENT"
1405
+ },
1406
+ /** The tenant's own id (= resource id). Drives the partition key. */
1407
+ tenantId: {
1408
+ type: "string",
1409
+ required: true
1410
+ },
1411
+ /** FHIR Resource.id; logical id in URL. Equals tenantId. */
1412
+ id: {
1413
+ type: "string",
1414
+ required: true
1415
+ },
1416
+ /** Full Tenant resource serialized as JSON string. */
1417
+ resource: {
1418
+ type: "string",
1419
+ required: true
1420
+ },
1421
+ /**
1422
+ * Summary projection (key display fields as JSON string: id, displayName, status).
1423
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
1424
+ */
1425
+ summary: {
1426
+ type: "string",
1427
+ required: true
1428
+ },
1429
+ /** Version id (e.g. ULID). */
1430
+ vid: {
1431
+ type: "string",
1432
+ required: true
1433
+ },
1434
+ lastUpdated: {
1435
+ type: "string",
1436
+ required: true
1437
+ },
1438
+ gsi1Shard: gsi1ShardAttribute,
1439
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
1440
+ gsi1sk: gsi1skAttribute,
1441
+ deleted: {
1442
+ type: "boolean",
1443
+ required: false
1444
+ },
1445
+ bundleId: {
1446
+ type: "string",
1447
+ required: false
1448
+ },
1449
+ msgId: {
1450
+ type: "string",
1451
+ required: false
1452
+ }
1453
+ },
1454
+ indexes: {
1455
+ /** Base table: PK = TENANT#ID#<tenantId>, SK = CURRENT. Do not supply PK or SK from outside. */
1456
+ record: {
1457
+ pk: {
1458
+ field: "PK",
1459
+ composite: ["tenantId"],
1460
+ template: "TENANT#ID#${tenantId}"
1461
+ },
1462
+ sk: {
1463
+ field: "SK",
1464
+ composite: ["sk"],
1465
+ template: "${sk}"
1466
+ }
1467
+ },
1468
+ /**
1469
+ * GSI1 — Unified Sharded List per ADR-011: list all Tenants across the four shards.
1470
+ * Tenant lives at the platform tier (no parent tenant or workspace), so `TID#-#WID#-`
1471
+ * sentinels precede `RT#Tenant#SHARD#<n>`. SK is derived via `gsi1skAttribute` —
1472
+ * `<normalizedName>#<id>` when the resource carries a `name`, else `<lastUpdated>#<id>`
1473
+ * (DR-004). `casing: "none"` preserves the normalized label and ISO-8601 `T`/`Z`.
1474
+ */
1475
+ gsi1: {
1476
+ index: "GSI1",
1477
+ pk: {
1478
+ field: "GSI1PK",
1479
+ composite: ["gsi1Shard"],
1480
+ template: "TID#-#WID#-#RT#Tenant#SHARD#${gsi1Shard}"
1481
+ },
1482
+ sk: {
1483
+ field: "GSI1SK",
1484
+ casing: "none",
1485
+ composite: ["gsi1sk"],
1486
+ template: "${gsi1sk}"
1487
+ }
1488
+ }
1489
+ }
1490
+ });
1491
+
1492
+ // src/data/dynamo/entities/control/user-entity.ts
1493
+ import { Entity as Entity12 } from "electrodb";
1494
+ var UserEntity = new Entity12({
1495
+ model: {
1496
+ entity: "user",
1497
+ service: "control",
1498
+ version: "01"
1499
+ },
1500
+ attributes: {
1501
+ /** Sort key sentinel. Always "CURRENT". */
1502
+ sk: {
1503
+ type: "string",
1504
+ required: true,
1505
+ default: "CURRENT"
1506
+ },
1507
+ /** FHIR Resource.id; platform user id (ohi_uid). */
1508
+ id: {
1509
+ type: "string",
1510
+ required: true
1511
+ },
1512
+ /** Full User resource serialized as JSON string. */
1513
+ resource: {
1514
+ type: "string",
1515
+ required: true
1516
+ },
1517
+ /**
1518
+ * Summary projection (key display fields as JSON string: id, displayName, status).
1519
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
1520
+ */
1521
+ summary: {
1522
+ type: "string",
1523
+ required: true
1524
+ },
1525
+ /**
1526
+ * Immutable Cognito-issued `sub` claim. Drives GSI2 (sub-lookup). Optional until the
1527
+ * Post Confirmation Lambda (#770) lands; required thereafter.
1528
+ */
1529
+ cognitoSub: {
1530
+ type: "string",
1531
+ required: false
1532
+ },
1533
+ /** Version id (e.g. ULID). */
1534
+ vid: {
1535
+ type: "string",
1536
+ required: true
1537
+ },
1538
+ lastUpdated: {
1539
+ type: "string",
1540
+ required: true
1541
+ },
1542
+ gsi1Shard: gsi1ShardAttribute,
1543
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
1544
+ gsi1sk: gsi1skAttribute,
1545
+ deleted: {
1546
+ type: "boolean",
1547
+ required: false
1548
+ },
1549
+ /**
1550
+ * TR-022 / ADR-018 lifecycle state for the cascade pipeline.
1551
+ *
1552
+ * - `active` (or undefined) — normal, readable state.
1553
+ * - `deleting` — intermediate state set synchronously by the
1554
+ * hard-delete API entry point. The owning-delete cascade state
1555
+ * machine fans out from this transition (DynamoDB stream →
1556
+ * `control-plane.owning-delete.v1` → Step Functions). Readers MUST
1557
+ * short-circuit on `deleting` so partial cascades stay invisible.
1558
+ * - `deleted-failed` — terminal failure state set by the cascade
1559
+ * finalize Lambda when the cascade run fails irrecoverably.
1560
+ * Operators recover by re-running the cascade or by direct
1561
+ * intervention.
1562
+ *
1563
+ * The cascade finalize step deletes the canonical record conditional
1564
+ * on `lifecycleState = "deleting"`; on replay the conditional check
1565
+ * fails and the finalize step treats that as a no-op success.
1566
+ */
1567
+ lifecycleState: {
1568
+ type: ["active", "deleting", "deleted-failed"],
1569
+ required: false
1570
+ },
1571
+ bundleId: {
1572
+ type: "string",
1573
+ required: false
1574
+ },
1575
+ msgId: {
1576
+ type: "string",
1577
+ required: false
1578
+ }
1579
+ },
1580
+ indexes: {
1581
+ /** Base table: PK = USER#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
1582
+ record: {
1583
+ pk: {
1584
+ field: "PK",
1585
+ composite: ["id"],
1586
+ template: "USER#ID#${id}"
1587
+ },
1588
+ sk: {
1589
+ field: "SK",
1590
+ composite: ["sk"],
1591
+ template: "${sk}"
1592
+ }
1593
+ },
1594
+ /**
1595
+ * GSI1 — Unified Sharded List per ADR-011: list all Users across the four shards.
1596
+ * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#User#SHARD#<n>`.
1597
+ * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
1598
+ * extractable (string `name`/`title` via introspection), else `<lastUpdated>#<id>`
1599
+ * (DR-004). `casing: "none"` preserves the normalized label and ISO-8601 `T`/`Z`.
1600
+ */
1601
+ gsi1: {
1602
+ index: "GSI1",
1603
+ pk: {
1604
+ field: "GSI1PK",
1605
+ composite: ["gsi1Shard"],
1606
+ template: "TID#-#WID#-#RT#User#SHARD#${gsi1Shard}"
1607
+ },
1608
+ sk: {
1609
+ field: "GSI1SK",
1610
+ casing: "none",
1611
+ composite: ["gsi1sk"],
1612
+ template: "${gsi1sk}"
1613
+ }
1614
+ },
1615
+ /**
1616
+ * GSI2 — Cognito sub-lookup per ADR-011: resolves the UserEntity from a Cognito `sub` claim.
1617
+ * `condition` skips the index when `cognitoSub` is missing so legacy items without a sub are
1618
+ * not indexed.
1619
+ */
1620
+ gsi2: {
1621
+ index: "GSI2",
1622
+ condition: (attrs) => typeof attrs.cognitoSub === "string" && attrs.cognitoSub.length > 0,
1623
+ pk: {
1624
+ field: "GSI2PK",
1625
+ casing: "none",
1626
+ composite: ["cognitoSub"],
1627
+ template: "USER#SUB#${cognitoSub}"
1628
+ },
1629
+ sk: {
1630
+ field: "GSI2SK",
1631
+ casing: "none",
1632
+ composite: [],
1633
+ template: "CURRENT"
1634
+ }
1635
+ }
1636
+ }
1637
+ });
1638
+
1639
+ // src/data/dynamo/entities/control/workspace-entity.ts
1640
+ import { Entity as Entity13 } from "electrodb";
1641
+ var WorkspaceEntity = new Entity13({
1642
+ model: {
1643
+ entity: "workspace",
1644
+ service: "control",
1645
+ version: "01"
1646
+ },
1647
+ attributes: {
1648
+ /** Sort key sentinel. Always "CURRENT". */
1649
+ sk: {
1650
+ type: "string",
1651
+ required: true,
1652
+ default: "CURRENT"
1653
+ },
1654
+ /** Tenant that contains this workspace (required). */
1655
+ tenantId: {
1656
+ type: "string",
1657
+ required: true
1658
+ },
1659
+ /** FHIR Resource.id; logical id in URL. */
1660
+ id: {
1661
+ type: "string",
1662
+ required: true
1663
+ },
1664
+ /** Full Workspace resource serialized as JSON string. */
1665
+ resource: {
1666
+ type: "string",
1667
+ required: true
1668
+ },
1669
+ /**
1670
+ * Summary projection (key display fields as JSON string: id, displayName, status).
1671
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
1672
+ */
1673
+ summary: {
1674
+ type: "string",
1675
+ required: true
1676
+ },
1677
+ /** Version id (e.g. ULID). */
1678
+ vid: {
1679
+ type: "string",
1680
+ required: true
1681
+ },
1682
+ lastUpdated: {
1683
+ type: "string",
1684
+ required: true
1685
+ },
1686
+ gsi1Shard: gsi1ShardAttribute,
1687
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
1688
+ gsi1sk: gsi1skAttribute,
1689
+ deleted: {
1690
+ type: "boolean",
1691
+ required: false
1692
+ },
1693
+ /**
1694
+ * TR-022 / ADR-018 lifecycle state for the cascade pipeline.
1695
+ *
1696
+ * - `active` (or undefined) — normal, readable state.
1697
+ * - `deleting` — intermediate state set synchronously by the
1698
+ * hard-delete API entry point. The owning-delete cascade state
1699
+ * machine fans out from this transition (DynamoDB stream →
1700
+ * `control-plane.owning-delete.v1` → Step Functions). Readers MUST
1701
+ * short-circuit on `deleting` so partial cascades stay invisible.
1702
+ * - `deleted-failed` — terminal failure state set by the cascade
1703
+ * finalize Lambda when the cascade run fails irrecoverably.
1704
+ * Operators recover by re-running the cascade or by direct
1705
+ * intervention.
1706
+ *
1707
+ * The cascade finalize step deletes the canonical record conditional
1708
+ * on `lifecycleState = "deleting"`; on replay the conditional check
1709
+ * fails and the finalize step treats that as a no-op success.
1710
+ */
1711
+ lifecycleState: {
1712
+ type: ["active", "deleting", "deleted-failed"],
1713
+ required: false
1714
+ },
1715
+ bundleId: {
1716
+ type: "string",
1717
+ required: false
1718
+ },
1719
+ msgId: {
1720
+ type: "string",
1721
+ required: false
1722
+ }
1723
+ },
1724
+ indexes: {
1725
+ /** Base table: PK = TID#<tenantId>#WORKSPACE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
1726
+ record: {
1727
+ pk: {
1728
+ field: "PK",
1729
+ composite: ["tenantId", "id"],
1730
+ template: "TID#${tenantId}#WORKSPACE#ID#${id}"
1731
+ },
1732
+ sk: {
1733
+ field: "SK",
1734
+ composite: ["sk"],
1735
+ template: "${sk}"
1736
+ }
1737
+ },
1738
+ /**
1739
+ * GSI1 — Unified Sharded List per ADR-011: list all Workspaces for a tenant across the
1740
+ * four shards. Workspace is itself the workspace identity, so `WID#-` is a sentinel.
1741
+ * SK is derived via `gsi1skAttribute` — `<normalizedName>#<id>` when the resource
1742
+ * carries a `name`, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves
1743
+ * the normalized label and ISO-8601 `T`/`Z`.
1744
+ */
1745
+ gsi1: {
1746
+ index: "GSI1",
1747
+ pk: {
1748
+ field: "GSI1PK",
1749
+ composite: ["tenantId", "gsi1Shard"],
1750
+ template: "TID#${tenantId}#WID#-#RT#Workspace#SHARD#${gsi1Shard}"
1751
+ },
1752
+ sk: {
1753
+ field: "GSI1SK",
1754
+ casing: "none",
1755
+ composite: ["gsi1sk"],
1756
+ template: "${gsi1sk}"
1757
+ }
1758
+ }
1759
+ }
1760
+ });
1761
+
1762
+ // src/data/dynamo/dynamo-control-service.ts
1763
+ var controlPlaneEntities = {
1764
+ configuration: ConfigurationEntity,
1765
+ configurationUserProjection: ConfigurationUserProjectionEntity,
1766
+ configurationWorkspaceProjection: ConfigurationWorkspaceProjectionEntity,
1767
+ membership: MembershipEntity,
1768
+ membershipUserProjection: MembershipUserProjectionEntity,
1769
+ membershipWorkspaceProjection: MembershipWorkspaceProjectionEntity,
1770
+ role: RoleEntity,
1771
+ roleAssignment: RoleAssignmentEntity,
1772
+ roleAssignmentUserProjection: RoleAssignmentUserProjectionEntity,
1773
+ roleAssignmentWorkspaceProjection: RoleAssignmentWorkspaceProjectionEntity,
1774
+ tenant: TenantEntity,
1775
+ user: UserEntity,
1776
+ workspace: WorkspaceEntity
1777
+ };
1778
+ var controlPlaneService = new Service(controlPlaneEntities, {
1779
+ table: defaultTableName,
1780
+ client: dynamoClient
1781
+ });
1782
+ var DynamoControlService = {
1783
+ entities: controlPlaneService.entities,
1784
+ transaction: controlPlaneService.transaction
1785
+ };
1786
+ function getDynamoControlService(tableName) {
1787
+ const resolved = tableName ?? defaultTableName;
1788
+ const service = new Service(controlPlaneEntities, {
1789
+ table: resolved,
1790
+ client: dynamoClient
1791
+ });
1792
+ return {
1793
+ entities: service.entities,
1794
+ transaction: service.transaction
1795
+ };
1796
+ }
1797
+
1798
+ export {
1799
+ SHARD_COUNT,
1800
+ computeShard,
1801
+ getDynamoControlService
1802
+ };
1803
+ //# sourceMappingURL=chunk-6NBGYGFL.mjs.map