@openhi/constructs 0.0.110 → 0.0.112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/lib/chunk-23PUSHBV.mjs +24 -0
  2. package/lib/chunk-23PUSHBV.mjs.map +1 -0
  3. package/lib/chunk-2O3CXY2C.mjs +79 -0
  4. package/lib/chunk-2O3CXY2C.mjs.map +1 -0
  5. package/lib/{chunk-7FUAMZOF.mjs → chunk-53OHXLIL.mjs} +3 -3
  6. package/lib/chunk-6NBGYGFL.mjs +1803 -0
  7. package/lib/chunk-6NBGYGFL.mjs.map +1 -0
  8. package/lib/chunk-7RZHFI77.mjs +22 -0
  9. package/lib/chunk-7RZHFI77.mjs.map +1 -0
  10. package/lib/{chunk-7Q2IJ2J5.mjs → chunk-CUUKXDB2.mjs} +6 -6
  11. package/lib/chunk-FYHBHHWK.mjs +47 -0
  12. package/lib/chunk-FYHBHHWK.mjs.map +1 -0
  13. package/lib/{chunk-MULKGFIJ.mjs → chunk-GBDIGTNV.mjs} +165 -10
  14. package/lib/chunk-GBDIGTNV.mjs.map +1 -0
  15. package/lib/chunk-HQ67J7BP.mjs +199 -0
  16. package/lib/chunk-HQ67J7BP.mjs.map +1 -0
  17. package/lib/{chunk-AJ3G3THO.mjs → chunk-KO64HPWQ.mjs} +2 -2
  18. package/lib/{chunk-BB5MK4L3.mjs → chunk-KSFC72TT.mjs} +3 -3
  19. package/lib/{chunk-2TPJ6HOF.mjs → chunk-NZRW7ROK.mjs} +72 -54
  20. package/lib/chunk-NZRW7ROK.mjs.map +1 -0
  21. package/lib/chunk-QJDHVMKT.mjs +117 -0
  22. package/lib/chunk-QJDHVMKT.mjs.map +1 -0
  23. package/lib/{chunk-IS4VQRI4.mjs → chunk-QMBJ4VHC.mjs} +12 -47
  24. package/lib/chunk-QMBJ4VHC.mjs.map +1 -0
  25. package/lib/chunk-TRY7JGWO.mjs +16 -0
  26. package/lib/chunk-TRY7JGWO.mjs.map +1 -0
  27. package/lib/chunk-W4KR4CSL.mjs +236 -0
  28. package/lib/chunk-W4KR4CSL.mjs.map +1 -0
  29. package/lib/{chunk-AGF3RAAZ.mjs → chunk-WPCBVDFZ.mjs} +2 -2
  30. package/lib/chunk-WQWFVEVX.mjs +66 -0
  31. package/lib/chunk-WQWFVEVX.mjs.map +1 -0
  32. package/lib/{chunk-SYBADQXI.mjs → chunk-ZM4GDHHC.mjs} +77 -2
  33. package/lib/chunk-ZM4GDHHC.mjs.map +1 -0
  34. package/lib/data-store-postgres-replication.handler.js +26 -17
  35. package/lib/data-store-postgres-replication.handler.js.map +1 -1
  36. package/lib/data-store-postgres-replication.handler.mjs +5 -65
  37. package/lib/data-store-postgres-replication.handler.mjs.map +1 -1
  38. package/lib/delete-chunk.handler.d.mts +29 -0
  39. package/lib/delete-chunk.handler.d.ts +29 -0
  40. package/lib/delete-chunk.handler.js +2716 -0
  41. package/lib/delete-chunk.handler.js.map +1 -0
  42. package/lib/delete-chunk.handler.mjs +47 -0
  43. package/lib/delete-chunk.handler.mjs.map +1 -0
  44. package/lib/events-CjS-sm0W.d.mts +107 -0
  45. package/lib/events-CjS-sm0W.d.ts +107 -0
  46. package/lib/events-Da_cFgtc.d.mts +208 -0
  47. package/lib/events-Da_cFgtc.d.ts +208 -0
  48. package/lib/finalize.handler.d.mts +35 -0
  49. package/lib/finalize.handler.d.ts +35 -0
  50. package/lib/finalize.handler.js +875 -0
  51. package/lib/finalize.handler.js.map +1 -0
  52. package/lib/finalize.handler.mjs +166 -0
  53. package/lib/finalize.handler.mjs.map +1 -0
  54. package/lib/index.d.mts +189 -2
  55. package/lib/index.d.ts +500 -3
  56. package/lib/index.js +1753 -174
  57. package/lib/index.js.map +1 -1
  58. package/lib/index.mjs +571 -17
  59. package/lib/index.mjs.map +1 -1
  60. package/lib/list-chunks.handler.d.mts +28 -0
  61. package/lib/list-chunks.handler.d.ts +28 -0
  62. package/lib/list-chunks.handler.js +2746 -0
  63. package/lib/list-chunks.handler.js.map +1 -0
  64. package/lib/list-chunks.handler.mjs +54 -0
  65. package/lib/list-chunks.handler.mjs.map +1 -0
  66. package/lib/platform-deploy-bridge.handler.js +76 -1
  67. package/lib/platform-deploy-bridge.handler.js.map +1 -1
  68. package/lib/platform-deploy-bridge.handler.mjs +1 -1
  69. package/lib/pre-token-generation.handler.js +1106 -155
  70. package/lib/pre-token-generation.handler.js.map +1 -1
  71. package/lib/pre-token-generation.handler.mjs +6 -4
  72. package/lib/pre-token-generation.handler.mjs.map +1 -1
  73. package/lib/provision-default-workspace.handler.js +1529 -142
  74. package/lib/provision-default-workspace.handler.js.map +1 -1
  75. package/lib/provision-default-workspace.handler.mjs +8 -4
  76. package/lib/provision-default-workspace.handler.mjs.map +1 -1
  77. package/lib/rename-finalize.handler.d.mts +30 -0
  78. package/lib/rename-finalize.handler.d.ts +30 -0
  79. package/lib/rename-finalize.handler.js +795 -0
  80. package/lib/rename-finalize.handler.js.map +1 -0
  81. package/lib/rename-finalize.handler.mjs +90 -0
  82. package/lib/rename-finalize.handler.mjs.map +1 -0
  83. package/lib/rename-list-targets.handler.d.mts +26 -0
  84. package/lib/rename-list-targets.handler.d.ts +26 -0
  85. package/lib/rename-list-targets.handler.js +2985 -0
  86. package/lib/rename-list-targets.handler.js.map +1 -0
  87. package/lib/rename-list-targets.handler.mjs +431 -0
  88. package/lib/rename-list-targets.handler.mjs.map +1 -0
  89. package/lib/rename-rewrite-chunk.handler.d.mts +35 -0
  90. package/lib/rename-rewrite-chunk.handler.d.ts +35 -0
  91. package/lib/rename-rewrite-chunk.handler.js +2021 -0
  92. package/lib/rename-rewrite-chunk.handler.js.map +1 -0
  93. package/lib/rename-rewrite-chunk.handler.mjs +27 -0
  94. package/lib/rename-rewrite-chunk.handler.mjs.map +1 -0
  95. package/lib/rest-api-lambda.handler.js +4087 -921
  96. package/lib/rest-api-lambda.handler.js.map +1 -1
  97. package/lib/rest-api-lambda.handler.mjs +1827 -81
  98. package/lib/rest-api-lambda.handler.mjs.map +1 -1
  99. package/lib/seed-demo-data.handler.js +1588 -124
  100. package/lib/seed-demo-data.handler.js.map +1 -1
  101. package/lib/seed-demo-data.handler.mjs +10 -6
  102. package/lib/seed-system-data.handler.js +1179 -155
  103. package/lib/seed-system-data.handler.js.map +1 -1
  104. package/lib/seed-system-data.handler.mjs +5 -4
  105. package/lib/seed-system-data.handler.mjs.map +1 -1
  106. package/package.json +1 -1
  107. package/lib/chunk-2TPJ6HOF.mjs.map +0 -1
  108. package/lib/chunk-IS4VQRI4.mjs.map +0 -1
  109. package/lib/chunk-MULKGFIJ.mjs.map +0 -1
  110. package/lib/chunk-QR5JVSCF.mjs +0 -862
  111. package/lib/chunk-QR5JVSCF.mjs.map +0 -1
  112. package/lib/chunk-SYBADQXI.mjs.map +0 -1
  113. /package/lib/{chunk-7FUAMZOF.mjs.map → chunk-53OHXLIL.mjs.map} +0 -0
  114. /package/lib/{chunk-7Q2IJ2J5.mjs.map → chunk-CUUKXDB2.mjs.map} +0 -0
  115. /package/lib/{chunk-AJ3G3THO.mjs.map → chunk-KO64HPWQ.mjs.map} +0 -0
  116. /package/lib/{chunk-BB5MK4L3.mjs.map → chunk-KSFC72TT.mjs.map} +0 -0
  117. /package/lib/{chunk-AGF3RAAZ.mjs.map → chunk-WPCBVDFZ.mjs.map} +0 -0
@@ -26,7 +26,7 @@ module.exports = __toCommonJS(pre_token_generation_handler_exports);
26
26
  var import_types6 = require("@openhi/types");
27
27
 
28
28
  // src/data/dynamo/dynamo-control-service.ts
29
- var import_electrodb8 = require("electrodb");
29
+ var import_electrodb14 = require("electrodb");
30
30
 
31
31
  // src/data/dynamo/dynamo-client.ts
32
32
  var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
@@ -90,6 +90,60 @@ var gsi1skAttribute = {
90
90
  return label !== void 0 ? `${label}#${id}` : fallback;
91
91
  }
92
92
  };
93
+ function extractRoleId(resource) {
94
+ const flat = resource.roleId;
95
+ if (typeof flat === "string" && flat.length > 0) return flat;
96
+ const role = resource.role;
97
+ if (role && typeof role === "object") {
98
+ const reference = role.reference;
99
+ if (typeof reference === "string" && reference.length > 0) {
100
+ const slash = reference.lastIndexOf("/");
101
+ const tail = slash >= 0 ? reference.slice(slash + 1) : reference;
102
+ if (tail.length > 0) return tail;
103
+ }
104
+ }
105
+ return void 0;
106
+ }
107
+ var roleAssignmentGsi1skAttribute = {
108
+ type: "string",
109
+ watch: ["resource", "denormalizedUserName", "lastUpdated", "id"],
110
+ set: (_val, item) => {
111
+ const id = typeof item?.id === "string" ? item.id : "";
112
+ const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
113
+ const fallback = `${lastUpdated}#${id}`;
114
+ if (typeof item?.resource !== "string" || item.resource.length === 0) {
115
+ return fallback;
116
+ }
117
+ let parsed;
118
+ try {
119
+ parsed = JSON.parse(item.resource);
120
+ } catch {
121
+ return fallback;
122
+ }
123
+ if (!parsed || typeof parsed !== "object") return fallback;
124
+ const roleId = extractRoleId(parsed);
125
+ if (roleId === void 0) return fallback;
126
+ const denormalizedUserName = typeof item.denormalizedUserName === "string" ? item.denormalizedUserName : "";
127
+ const normalizedUserName = denormalizedUserName.length > 0 ? (0, import_types.normalizeLabel)(denormalizedUserName) : "";
128
+ if (normalizedUserName.length === 0) return fallback;
129
+ return `${roleId}#${normalizedUserName}#${id}`;
130
+ }
131
+ };
132
+ var membershipGsi1skAttribute = {
133
+ type: "string",
134
+ watch: ["denormalizedUserName", "lastUpdated", "id"],
135
+ set: (_val, item) => {
136
+ const id = typeof item?.id === "string" ? item.id : "";
137
+ const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
138
+ const fallback = `${lastUpdated}#${id}`;
139
+ const denormalizedUserName = typeof item?.denormalizedUserName === "string" ? item.denormalizedUserName : "";
140
+ const normalizedUserName = denormalizedUserName.length > 0 ? (0, import_types.normalizeLabel)(denormalizedUserName) : "";
141
+ if (normalizedUserName.length === 0) {
142
+ return fallback;
143
+ }
144
+ return `${normalizedUserName}#${id}`;
145
+ }
146
+ };
93
147
 
94
148
  // src/data/dynamo/entities/control/configuration-entity.ts
95
149
  var ConfigurationEntity = new import_electrodb.Entity({
@@ -134,25 +188,743 @@ var ConfigurationEntity = new import_electrodb.Entity({
134
188
  type: "string",
135
189
  required: true
136
190
  },
137
- /** FHIR Resource.id; logical id in URL and for the Configuration resource. */
191
+ /** FHIR Resource.id; logical id in URL and for the Configuration resource. */
192
+ id: {
193
+ type: "string",
194
+ required: true
195
+ },
196
+ /** Payload as JSON string. JSON.stringify(resource) on write; JSON.parse(item.resource) on read. */
197
+ resource: {
198
+ type: "string",
199
+ required: true
200
+ },
201
+ /**
202
+ * Summary projection (key display fields as JSON string: id, key, status).
203
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
204
+ */
205
+ summary: {
206
+ type: "string",
207
+ required: true
208
+ },
209
+ /** Version id (e.g. ULID). Tracks current version; S3 history key. */
210
+ vid: {
211
+ type: "string",
212
+ required: true
213
+ },
214
+ lastUpdated: {
215
+ type: "string",
216
+ required: true
217
+ },
218
+ gsi1Shard: gsi1ShardAttribute,
219
+ deleted: {
220
+ type: "boolean",
221
+ required: false
222
+ },
223
+ bundleId: {
224
+ type: "string",
225
+ required: false
226
+ },
227
+ msgId: {
228
+ type: "string",
229
+ required: false
230
+ }
231
+ },
232
+ indexes: {
233
+ /** 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. */
234
+ record: {
235
+ pk: {
236
+ field: "PK",
237
+ composite: ["tenantId", "workspaceId", "userId", "roleId"],
238
+ template: "CONFIG#TID#${tenantId}#WID#${workspaceId}#UID#${userId}#RID#${roleId}"
239
+ },
240
+ sk: {
241
+ field: "SK",
242
+ composite: ["key", "sk"],
243
+ template: "KEY#${key}#SK#${sk}"
244
+ }
245
+ },
246
+ /**
247
+ * GSI1 — Unified Sharded List per ADR-011: list all Configuration entries for a
248
+ * (tenant, workspace) across the four shards. Use for "list configs scoped to this tenant"
249
+ * (workspaceId = "-") or "list configs scoped to this workspace". Does not support
250
+ * hierarchical resolution in one query; use base table GetItem in fallback order
251
+ * (user → workspace → tenant → baseline) for that.
252
+ * SK is `<key>#<id>` — Configuration's `key` is a required entity attribute (the
253
+ * config category: endpoints, branding, display, …) and the natural sort/lookup
254
+ * dimension. `casing: "none"` preserves the literal key value.
255
+ */
256
+ gsi1: {
257
+ index: "GSI1",
258
+ pk: {
259
+ field: "GSI1PK",
260
+ composite: ["tenantId", "workspaceId", "gsi1Shard"],
261
+ template: "TID#${tenantId}#WID#${workspaceId}#RT#Configuration#SHARD#${gsi1Shard}"
262
+ },
263
+ sk: {
264
+ field: "GSI1SK",
265
+ casing: "none",
266
+ composite: ["key", "id"],
267
+ template: "${key}#${id}"
268
+ }
269
+ }
270
+ }
271
+ });
272
+
273
+ // src/data/dynamo/entities/control/configuration-user-projection-entity.ts
274
+ var import_electrodb2 = require("electrodb");
275
+ var ConfigurationUserProjectionEntity = new import_electrodb2.Entity({
276
+ model: {
277
+ entity: "configurationUserProjection",
278
+ service: "control",
279
+ version: "01"
280
+ },
281
+ attributes: {
282
+ /**
283
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
284
+ * base-table PK. Always required — the projection has no meaning
285
+ * outside a user partition.
286
+ */
287
+ userId: {
288
+ type: "string",
289
+ required: true
290
+ },
291
+ /**
292
+ * Pre-composed sort key — built by the operations-layer projection
293
+ * writer via `buildConfigurationUserProjectionSk`. The entity stores
294
+ * the value verbatim so the SK grammar (pattern #10 user-scope) is
295
+ * owned by the operations layer, not duplicated here.
296
+ */
297
+ sk: {
298
+ type: "string",
299
+ required: true
300
+ },
301
+ /**
302
+ * Configuration canonical-record id. Stored as a discriminating
303
+ * field so consumers can hydrate the canonical row via the
304
+ * Configuration get-by-id operation when the projection's `summary`
305
+ * is insufficient.
306
+ */
307
+ configurationId: {
308
+ type: "string",
309
+ required: true
310
+ },
311
+ /**
312
+ * Tenant the Configuration is associated with. The canonical row
313
+ * keys off `(tenantId, workspaceId, userId, roleId)`; the projection
314
+ * carries `tenantId` so consumers reconstructing the canonical PK
315
+ * have the tenant segment without a hop.
316
+ */
317
+ tenantId: {
318
+ type: "string",
319
+ required: true
320
+ },
321
+ /**
322
+ * Scope marker. Always `"user"` on this projection — recorded
323
+ * explicitly so future scope-bearing projections (workspace,
324
+ * tenant, role) can share filter semantics in a unified
325
+ * cross-projection list query if one ever lands.
326
+ */
327
+ scope: {
328
+ type: "string",
329
+ required: true,
330
+ default: "user"
331
+ },
332
+ /**
333
+ * Configuration's `key` attribute (config category, e.g. endpoints,
334
+ * branding, display). Mirrored from the canonical row so consumers
335
+ * reading the projection get the natural display label without a
336
+ * BatchGet hop. Doubles as the source of `<normalizedConfigName>` in
337
+ * the SK.
338
+ */
339
+ displayName: {
340
+ type: "string",
341
+ required: false
342
+ },
343
+ /**
344
+ * Summary projection (key display fields as JSON string) — mirrored
345
+ * from the canonical Configuration row so user-partition queries do
346
+ * not need a BatchGet hop.
347
+ */
348
+ summary: {
349
+ type: "string",
350
+ required: true
351
+ },
352
+ /** Version id mirrored from the canonical Configuration row. */
353
+ vid: {
354
+ type: "string",
355
+ required: true
356
+ },
357
+ /** Last-updated timestamp mirrored from the canonical Configuration row. */
358
+ lastUpdated: {
359
+ type: "string",
360
+ required: true
361
+ }
362
+ },
363
+ indexes: {
364
+ /**
365
+ * Base table: PK = USER#ID#<userId>, SK = operation-supplied. A
366
+ * single `Query(PK = USER#ID#<userId>, SK begins_with
367
+ * 'CONFIGURATION#')` returns the user's user-scoped Configurations
368
+ * sorted by `<normalizedConfigName>` (then `<configurationId>` as
369
+ * the tiebreaker).
370
+ */
371
+ record: {
372
+ pk: {
373
+ field: "PK",
374
+ composite: ["userId"],
375
+ template: "USER#ID#${userId}"
376
+ },
377
+ sk: {
378
+ field: "SK",
379
+ casing: "none",
380
+ composite: ["sk"],
381
+ template: "${sk}"
382
+ }
383
+ }
384
+ }
385
+ });
386
+
387
+ // src/data/dynamo/entities/control/configuration-workspace-projection-entity.ts
388
+ var import_electrodb3 = require("electrodb");
389
+ var ConfigurationWorkspaceProjectionEntity = new import_electrodb3.Entity({
390
+ model: {
391
+ entity: "configurationWorkspaceProjection",
392
+ service: "control",
393
+ version: "01"
394
+ },
395
+ attributes: {
396
+ /**
397
+ * Tenant the workspace belongs to. Renders as the leading segment
398
+ * of the base-table PK. Always required — the workspace partition
399
+ * is tenant-scoped per ADR-011.
400
+ */
401
+ tenantId: {
402
+ type: "string",
403
+ required: true
404
+ },
405
+ /**
406
+ * Workspace partition discriminator. Renders as the trailing
407
+ * segment of the base-table PK
408
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
409
+ * the projection has no meaning outside a workspace partition.
410
+ */
411
+ workspaceId: {
412
+ type: "string",
413
+ required: true
414
+ },
415
+ /**
416
+ * Pre-composed sort key — built by the operations-layer projection
417
+ * writer via `buildConfigurationWorkspaceProjectionSk`. The entity
418
+ * stores the value verbatim so the SK grammar (pattern #10
419
+ * workspace-scope) is owned by the operations layer, not
420
+ * duplicated here.
421
+ */
422
+ sk: {
423
+ type: "string",
424
+ required: true
425
+ },
426
+ /**
427
+ * Configuration canonical-record id. Stored as a discriminating
428
+ * field so consumers can hydrate the canonical row via the
429
+ * Configuration get-by-id operation when the projection's `summary`
430
+ * is insufficient.
431
+ */
432
+ configurationId: {
433
+ type: "string",
434
+ required: true
435
+ },
436
+ /**
437
+ * Scope marker. Always `"workspace"` on this projection — recorded
438
+ * explicitly so future scope-bearing projections (user, tenant,
439
+ * role) can share filter semantics in a unified cross-projection
440
+ * list query if one ever lands.
441
+ */
442
+ scope: {
443
+ type: "string",
444
+ required: true,
445
+ default: "workspace"
446
+ },
447
+ /**
448
+ * Configuration's `key` attribute (config category, e.g. endpoints,
449
+ * branding, display). Mirrored from the canonical row so consumers
450
+ * reading the projection get the natural display label without a
451
+ * BatchGet hop. Doubles as the source of `<normalizedConfigName>`
452
+ * in the SK.
453
+ */
454
+ displayName: {
455
+ type: "string",
456
+ required: false
457
+ },
458
+ /**
459
+ * Summary projection (key display fields as JSON string) — mirrored
460
+ * from the canonical Configuration row so workspace-partition
461
+ * queries do not need a BatchGet hop.
462
+ */
463
+ summary: {
464
+ type: "string",
465
+ required: true
466
+ },
467
+ /** Version id mirrored from the canonical Configuration row. */
468
+ vid: {
469
+ type: "string",
470
+ required: true
471
+ },
472
+ /** Last-updated timestamp mirrored from the canonical Configuration row. */
473
+ lastUpdated: {
474
+ type: "string",
475
+ required: true
476
+ }
477
+ },
478
+ indexes: {
479
+ /**
480
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
481
+ * SK = operation-supplied. A single
482
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'CONFIGURATION#')`
483
+ * returns the workspace's workspace-scoped Configurations sorted by
484
+ * `<normalizedConfigName>` (then `<configurationId>` as the
485
+ * tiebreaker).
486
+ */
487
+ record: {
488
+ pk: {
489
+ field: "PK",
490
+ composite: ["tenantId", "workspaceId"],
491
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
492
+ },
493
+ sk: {
494
+ field: "SK",
495
+ casing: "none",
496
+ composite: ["sk"],
497
+ template: "${sk}"
498
+ }
499
+ }
500
+ }
501
+ });
502
+
503
+ // src/data/dynamo/entities/control/membership-entity.ts
504
+ var import_electrodb4 = require("electrodb");
505
+ var MembershipEntity = new import_electrodb4.Entity({
506
+ model: {
507
+ entity: "membership",
508
+ service: "control",
509
+ version: "01"
510
+ },
511
+ attributes: {
512
+ /** Sort key sentinel. Always "CURRENT". */
513
+ sk: {
514
+ type: "string",
515
+ required: true,
516
+ default: "CURRENT"
517
+ },
518
+ /** Tenant in which the user has membership (required). */
519
+ tenantId: {
520
+ type: "string",
521
+ required: true
522
+ },
523
+ /** FHIR Resource.id; membership id. */
524
+ id: {
525
+ type: "string",
526
+ required: true
527
+ },
528
+ /** Full Membership resource serialized as JSON string. */
529
+ resource: {
530
+ type: "string",
531
+ required: true
532
+ },
533
+ /**
534
+ * Summary projection (key display fields as JSON string: id, displayName, status).
535
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
536
+ */
537
+ summary: {
538
+ type: "string",
539
+ required: true
540
+ },
541
+ /** Version id (e.g. ULID). */
542
+ vid: {
543
+ type: "string",
544
+ required: true
545
+ },
546
+ lastUpdated: {
547
+ type: "string",
548
+ required: true
549
+ },
550
+ gsi1Shard: gsi1ShardAttribute,
551
+ /**
552
+ * Derived GSI1 sort key — `<normalizedUserName>#<id>` per ADR-018
553
+ * pattern #1 so a GSI1 query partitioned on the tenant range-scans
554
+ * by user-name prefix and returns memberships sorted by user name.
555
+ * Falls back to `<lastUpdated>#<id>` when `denormalizedUserName`
556
+ * is missing.
557
+ */
558
+ gsi1sk: membershipGsi1skAttribute,
559
+ deleted: {
560
+ type: "boolean",
561
+ required: false
562
+ },
563
+ bundleId: {
564
+ type: "string",
565
+ required: false
566
+ },
567
+ msgId: {
568
+ type: "string",
569
+ required: false
570
+ },
571
+ /**
572
+ * Denormalized `linked-data-identity` Reference (e.g. `Practitioner/abc`).
573
+ * Populated from the FHIR extension on the Membership resource at write
574
+ * time so future GSIs can index data-plane identity lookups without
575
+ * deserializing the full resource JSON. See ADR 2026-03-13-02 §6.
576
+ */
577
+ linkedDataIdentityRef: {
578
+ type: "string",
579
+ required: false
580
+ },
581
+ /**
582
+ * Denormalized display name of the linked Tenant, captured at row
583
+ * last-write time. Promoted to a top-level attribute so the ADR-018
584
+ * adjacency-list projection SKs (pattern #3 — `MEMBERSHIP#TENANT#<normalizedTenantName>#…`)
585
+ * can be composed from a top-level field instead of digging into the
586
+ * `resource` JSON. Optional on the schema so pre-TR-024 rows do not
587
+ * break; the operations-layer multi-write helper (#1010) makes the
588
+ * field load-bearing at write time per TR-024 rule 2 (write-time
589
+ * source = canonical Tenant.displayName).
590
+ * @see TR-024 — Denormalized display-name attributes
591
+ */
592
+ denormalizedTenantName: {
593
+ type: "string",
594
+ required: false
595
+ },
596
+ /**
597
+ * Denormalized display name of the linked User, captured at row
598
+ * last-write time. Promoted to a top-level attribute so the ADR-018
599
+ * adjacency-list canonical-record GSI1SK (pattern #1 —
600
+ * `<normalizedUserName>#<id>`) and workspace-projection SK (pattern #2)
601
+ * can be composed from a top-level field. Optional on the schema so
602
+ * pre-TR-024 rows do not break; the operations-layer multi-write helper
603
+ * (#1010) makes the field load-bearing at write time per TR-024 rule 2
604
+ * (write-time source = canonical User.displayName).
605
+ * @see TR-024 — Denormalized display-name attributes
606
+ */
607
+ denormalizedUserName: {
608
+ type: "string",
609
+ required: false
610
+ }
611
+ },
612
+ indexes: {
613
+ /** Base table: PK = TID#<tenantId>#MEMBERSHIP#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
614
+ record: {
615
+ pk: {
616
+ field: "PK",
617
+ composite: ["tenantId", "id"],
618
+ template: "TID#${tenantId}#MEMBERSHIP#ID#${id}"
619
+ },
620
+ sk: {
621
+ field: "SK",
622
+ composite: ["sk"],
623
+ template: "${sk}"
624
+ }
625
+ },
626
+ /**
627
+ * GSI1 — Unified Sharded List per ADR-011: list all Memberships for a tenant across the
628
+ * four shards. Membership is tenant-scoped only, so `WID#-` is a sentinel.
629
+ * SK is derived via `membershipGsi1skAttribute` — composes
630
+ * `<normalizedUserName>#<id>` per ADR-018 pattern #1 (users in a
631
+ * tenant, sorted by user name); falls back to `<lastUpdated>#<id>`
632
+ * when `denormalizedUserName` is missing. `casing: "none"` preserves
633
+ * the normalized label and ISO-8601 `T`/`Z`.
634
+ */
635
+ gsi1: {
636
+ index: "GSI1",
637
+ pk: {
638
+ field: "GSI1PK",
639
+ composite: ["tenantId", "gsi1Shard"],
640
+ template: "TID#${tenantId}#WID#-#RT#Membership#SHARD#${gsi1Shard}"
641
+ },
642
+ sk: {
643
+ field: "GSI1SK",
644
+ casing: "none",
645
+ composite: ["gsi1sk"],
646
+ template: "${gsi1sk}"
647
+ }
648
+ }
649
+ }
650
+ });
651
+
652
+ // src/data/dynamo/entities/control/membership-user-projection-entity.ts
653
+ var import_electrodb5 = require("electrodb");
654
+ var MembershipUserProjectionEntity = new import_electrodb5.Entity({
655
+ model: {
656
+ entity: "membershipUserProjection",
657
+ service: "control",
658
+ version: "01"
659
+ },
660
+ attributes: {
661
+ /**
662
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
663
+ * base-table PK. Always required — the projection has no meaning
664
+ * outside a user partition.
665
+ */
666
+ userId: {
667
+ type: "string",
668
+ required: true
669
+ },
670
+ /**
671
+ * Pre-composed sort key — built by the operations-layer projection
672
+ * writer via `buildMembershipUserProjectionSk*` helpers. The entity
673
+ * stores the value verbatim so the SK grammar (patterns #3 and #4)
674
+ * is owned by the operations layer, not duplicated here.
675
+ */
676
+ sk: {
677
+ type: "string",
678
+ required: true
679
+ },
680
+ /** Tenant in which the membership applies. Always required. */
681
+ tenantId: {
682
+ type: "string",
683
+ required: true
684
+ },
685
+ /**
686
+ * Workspace the membership scopes to. Present iff the projection
687
+ * row is a pattern-#4 workspace sub-lane row; absent for pattern-#3
688
+ * tenant sub-lane rows.
689
+ */
690
+ workspaceId: {
691
+ type: "string",
692
+ required: false
693
+ },
694
+ /**
695
+ * Membership canonical-record id. Stored as a discriminating field
696
+ * so consumers can hydrate the canonical row via
697
+ * `MembershipEntity.get({ tenantId, id: membershipId })` when the
698
+ * projection's `summary` is insufficient.
699
+ */
700
+ membershipId: {
701
+ type: "string",
702
+ required: true
703
+ },
704
+ /**
705
+ * Summary projection (key display fields as JSON string: id,
706
+ * displayName, status) — mirrored from the canonical Membership row
707
+ * so user-partition queries do not need a BatchGet hop.
708
+ */
709
+ summary: {
710
+ type: "string",
711
+ required: true
712
+ },
713
+ /** Version id mirrored from the canonical Membership row. */
714
+ vid: {
715
+ type: "string",
716
+ required: true
717
+ },
718
+ /** Last-updated timestamp mirrored from the canonical Membership row. */
719
+ lastUpdated: {
720
+ type: "string",
721
+ required: true
722
+ },
723
+ /**
724
+ * Denormalized Tenant display name — required to compose pattern-#3
725
+ * SK (`MEMBERSHIP#TENANT#<normalizedTenantName>#…`). Optional on the
726
+ * schema because pre-TR-024 rows may not carry a display name; the
727
+ * operations layer falls back gracefully when missing.
728
+ */
729
+ denormalizedTenantName: {
730
+ type: "string",
731
+ required: false
732
+ },
733
+ /**
734
+ * Denormalized User display name — mirrored from the canonical
735
+ * Membership row per TR-024 rule 3 (canonical-record symmetry).
736
+ * Carried on the projection so consumers can render the user's
737
+ * display name without a hop to the User record.
738
+ */
739
+ denormalizedUserName: {
740
+ type: "string",
741
+ required: false
742
+ },
743
+ /**
744
+ * Denormalized Workspace display name — required to compose
745
+ * pattern-#4 SK (`MEMBERSHIP#WORKSPACE#TID#<tenantId>#<normalizedWorkspaceName>#…`).
746
+ * Optional on the schema (TR-024 § Open Item #4 defers a formal
747
+ * Workspace-rename cascade); the operations layer falls back to a
748
+ * sentinel when missing so the SK still has a valid shape.
749
+ */
750
+ denormalizedWorkspaceName: {
751
+ type: "string",
752
+ required: false
753
+ }
754
+ },
755
+ indexes: {
756
+ /**
757
+ * Base table: PK = USER#ID#<userId>, SK = operation-supplied.
758
+ * Both pattern #3 and pattern #4 use this same index — the SK string
759
+ * encodes the lane discriminator (`MEMBERSHIP#TENANT#…` vs
760
+ * `MEMBERSHIP#WORKSPACE#…`) so a single `Query(PK = USER#ID#<userId>,
761
+ * SK begins_with 'MEMBERSHIP#')` returns both lanes interleaved.
762
+ */
763
+ record: {
764
+ pk: {
765
+ field: "PK",
766
+ composite: ["userId"],
767
+ template: "USER#ID#${userId}"
768
+ },
769
+ sk: {
770
+ field: "SK",
771
+ casing: "none",
772
+ composite: ["sk"],
773
+ template: "${sk}"
774
+ }
775
+ }
776
+ }
777
+ });
778
+
779
+ // src/data/dynamo/entities/control/membership-workspace-projection-entity.ts
780
+ var import_electrodb6 = require("electrodb");
781
+ var MembershipWorkspaceProjectionEntity = new import_electrodb6.Entity({
782
+ model: {
783
+ entity: "membershipWorkspaceProjection",
784
+ service: "control",
785
+ version: "01"
786
+ },
787
+ attributes: {
788
+ /**
789
+ * Tenant the workspace belongs to. Renders as the leading segment
790
+ * of the base-table PK. Always required — the workspace partition
791
+ * is tenant-scoped per ADR-011.
792
+ */
793
+ tenantId: {
794
+ type: "string",
795
+ required: true
796
+ },
797
+ /**
798
+ * Workspace partition discriminator. Renders as the trailing
799
+ * segment of the base-table PK
800
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
801
+ * the projection has no meaning outside a workspace partition.
802
+ */
803
+ workspaceId: {
804
+ type: "string",
805
+ required: true
806
+ },
807
+ /**
808
+ * Pre-composed sort key — built by the operations-layer projection
809
+ * writer via `buildMembershipWorkspaceProjectionSk`. The entity
810
+ * stores the value verbatim so the SK grammar (pattern #2) is
811
+ * owned by the operations layer, not duplicated here.
812
+ */
813
+ sk: {
814
+ type: "string",
815
+ required: true
816
+ },
817
+ /**
818
+ * User the membership links. Stored as a discriminating field so
819
+ * consumers can hydrate the canonical User row via
820
+ * `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
821
+ * projection's `summary` is insufficient.
822
+ */
823
+ userId: {
824
+ type: "string",
825
+ required: true
826
+ },
827
+ /**
828
+ * Membership canonical-record id. Stored as a discriminating field
829
+ * so consumers can hydrate the canonical row via
830
+ * `MembershipEntity.get({ tenantId, id: membershipId })` when the
831
+ * projection's `summary` is insufficient.
832
+ */
833
+ membershipId: {
834
+ type: "string",
835
+ required: true
836
+ },
837
+ /**
838
+ * Summary projection (key display fields as JSON string: id,
839
+ * displayName, status) — mirrored from the canonical Membership row
840
+ * so workspace-partition queries do not need a BatchGet hop.
841
+ */
842
+ summary: {
843
+ type: "string",
844
+ required: true
845
+ },
846
+ /** Version id mirrored from the canonical Membership row. */
847
+ vid: {
848
+ type: "string",
849
+ required: true
850
+ },
851
+ /** Last-updated timestamp mirrored from the canonical Membership row. */
852
+ lastUpdated: {
853
+ type: "string",
854
+ required: true
855
+ },
856
+ /**
857
+ * Denormalized User display name — required to compose the
858
+ * pattern-#2 SK (`MEMBERSHIP#<normalizedUserName>#…`). Optional on
859
+ * the schema because pre-TR-024 rows may not carry a display name;
860
+ * the operations layer falls back to a sentinel when missing so
861
+ * the SK still has a valid shape. The TR-023 rename-cascade
862
+ * pipeline rewrites the SK on a User rename.
863
+ */
864
+ denormalizedUserName: {
865
+ type: "string",
866
+ required: false
867
+ }
868
+ },
869
+ indexes: {
870
+ /**
871
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
872
+ * SK = operation-supplied. Pattern #2 uses this index — the SK
873
+ * encodes the entity-type prefix (`MEMBERSHIP#…`) so a
874
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'MEMBERSHIP#')`
875
+ * returns every member projection for the workspace in normalized-
876
+ * user-name sort order.
877
+ */
878
+ record: {
879
+ pk: {
880
+ field: "PK",
881
+ composite: ["tenantId", "workspaceId"],
882
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
883
+ },
884
+ sk: {
885
+ field: "SK",
886
+ casing: "none",
887
+ composite: ["sk"],
888
+ template: "${sk}"
889
+ }
890
+ }
891
+ }
892
+ });
893
+
894
+ // src/data/dynamo/entities/control/role-entity.ts
895
+ var import_electrodb7 = require("electrodb");
896
+ var RoleEntity = new import_electrodb7.Entity({
897
+ model: {
898
+ entity: "role",
899
+ service: "control",
900
+ version: "01"
901
+ },
902
+ attributes: {
903
+ /** Sort key sentinel. Always "CURRENT". */
904
+ sk: {
905
+ type: "string",
906
+ required: true,
907
+ default: "CURRENT"
908
+ },
909
+ /** FHIR Resource.id; role id. */
138
910
  id: {
139
911
  type: "string",
140
912
  required: true
141
913
  },
142
- /** Payload as JSON string. JSON.stringify(resource) on write; JSON.parse(item.resource) on read. */
914
+ /** Full Role resource serialized as JSON string. */
143
915
  resource: {
144
916
  type: "string",
145
917
  required: true
146
918
  },
147
919
  /**
148
- * Summary projection (key display fields as JSON string: id, key, status).
920
+ * Summary projection (key display fields as JSON string: id, displayName, status).
149
921
  * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
150
922
  */
151
923
  summary: {
152
924
  type: "string",
153
925
  required: true
154
926
  },
155
- /** Version id (e.g. ULID). Tracks current version; S3 history key. */
927
+ /** Version id (e.g. ULID). */
156
928
  vid: {
157
929
  type: "string",
158
930
  required: true
@@ -162,6 +934,8 @@ var ConfigurationEntity = new import_electrodb.Entity({
162
934
  required: true
163
935
  },
164
936
  gsi1Shard: gsi1ShardAttribute,
937
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
938
+ gsi1sk: gsi1skAttribute,
165
939
  deleted: {
166
940
  type: "boolean",
167
941
  required: false
@@ -176,51 +950,48 @@ var ConfigurationEntity = new import_electrodb.Entity({
176
950
  }
177
951
  },
178
952
  indexes: {
179
- /** 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. */
953
+ /** Base table: PK = ROLE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
180
954
  record: {
181
955
  pk: {
182
956
  field: "PK",
183
- composite: ["tenantId", "workspaceId", "userId", "roleId"],
184
- template: "CONFIG#TID#${tenantId}#WID#${workspaceId}#UID#${userId}#RID#${roleId}"
957
+ composite: ["id"],
958
+ template: "ROLE#ID#${id}"
185
959
  },
186
960
  sk: {
187
961
  field: "SK",
188
- composite: ["key", "sk"],
189
- template: "KEY#${key}#SK#${sk}"
962
+ composite: ["sk"],
963
+ template: "${sk}"
190
964
  }
191
965
  },
192
966
  /**
193
- * GSI1 — Unified Sharded List per ADR-011: list all Configuration entries for a
194
- * (tenant, workspace) across the four shards. Use for "list configs scoped to this tenant"
195
- * (workspaceId = "-") or "list configs scoped to this workspace". Does not support
196
- * hierarchical resolution in one query; use base table GetItem in fallback order
197
- * (user workspace tenant → baseline) for that.
198
- * SK is `<key>#<id>` — Configuration's `key` is a required entity attribute (the
199
- * config category: endpoints, branding, display, …) and the natural sort/lookup
200
- * dimension. `casing: "none"` preserves the literal key value.
967
+ * GSI1 — Unified Sharded List per ADR-011: list all Roles across the four shards.
968
+ * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#Role#SHARD#<n>`.
969
+ * SK is derived via `gsi1skAttribute` uses the resource's natural label when
970
+ * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
971
+ * normalized label and ISO-8601 `T`/`Z`.
201
972
  */
202
973
  gsi1: {
203
974
  index: "GSI1",
204
975
  pk: {
205
976
  field: "GSI1PK",
206
- composite: ["tenantId", "workspaceId", "gsi1Shard"],
207
- template: "TID#${tenantId}#WID#${workspaceId}#RT#Configuration#SHARD#${gsi1Shard}"
977
+ composite: ["gsi1Shard"],
978
+ template: "TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}"
208
979
  },
209
980
  sk: {
210
981
  field: "GSI1SK",
211
982
  casing: "none",
212
- composite: ["key", "id"],
213
- template: "${key}#${id}"
983
+ composite: ["gsi1sk"],
984
+ template: "${gsi1sk}"
214
985
  }
215
986
  }
216
987
  }
217
988
  });
218
989
 
219
- // src/data/dynamo/entities/control/membership-entity.ts
220
- var import_electrodb2 = require("electrodb");
221
- var MembershipEntity = new import_electrodb2.Entity({
990
+ // src/data/dynamo/entities/control/roleassignment-entity.ts
991
+ var import_electrodb8 = require("electrodb");
992
+ var RoleAssignmentEntity = new import_electrodb8.Entity({
222
993
  model: {
223
- entity: "membership",
994
+ entity: "roleassignment",
224
995
  service: "control",
225
996
  version: "01"
226
997
  },
@@ -231,17 +1002,17 @@ var MembershipEntity = new import_electrodb2.Entity({
231
1002
  required: true,
232
1003
  default: "CURRENT"
233
1004
  },
234
- /** Tenant in which the user has membership (required). */
1005
+ /** Tenant in which the role assignment applies (required). */
235
1006
  tenantId: {
236
1007
  type: "string",
237
1008
  required: true
238
1009
  },
239
- /** FHIR Resource.id; membership id. */
1010
+ /** FHIR Resource.id; role assignment id. */
240
1011
  id: {
241
1012
  type: "string",
242
1013
  required: true
243
1014
  },
244
- /** Full Membership resource serialized as JSON string. */
1015
+ /** Full RoleAssignment resource serialized as JSON string. */
245
1016
  resource: {
246
1017
  type: "string",
247
1018
  required: true
@@ -264,8 +1035,15 @@ var MembershipEntity = new import_electrodb2.Entity({
264
1035
  required: true
265
1036
  },
266
1037
  gsi1Shard: gsi1ShardAttribute,
267
- /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
268
- gsi1sk: gsi1skAttribute,
1038
+ /**
1039
+ * Derived GSI1 sort key — discriminator-first
1040
+ * `<roleId>#<normalizedUserName>#<id>` per ADR-018 pattern #8 so a
1041
+ * GSI1 query partitioned on the tenant can `begins_with('<roleId>#')`
1042
+ * to enumerate every user assigned to a given role, sorted by user
1043
+ * name. Falls back to `<lastUpdated>#<id>` when either component is
1044
+ * missing.
1045
+ */
1046
+ gsi1sk: roleAssignmentGsi1skAttribute,
269
1047
  deleted: {
270
1048
  type: "boolean",
271
1049
  required: false
@@ -279,23 +1057,60 @@ var MembershipEntity = new import_electrodb2.Entity({
279
1057
  required: false
280
1058
  },
281
1059
  /**
282
- * Denormalized `linked-data-identity` Reference (e.g. `Practitioner/abc`).
283
- * Populated from the FHIR extension on the Membership resource at write
284
- * time so future GSIs can index data-plane identity lookups without
285
- * deserializing the full resource JSON. See ADR 2026-03-13-02 §6.
1060
+ * Denormalized display name of the linked Tenant, captured at row
1061
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1062
+ * adjacency-list user-projection SK (pattern #5
1063
+ * `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#<roleId>#TID#<tenantId>#<id>`)
1064
+ * can be composed from a top-level field instead of digging into the
1065
+ * `resource` JSON. Optional on the schema so pre-TR-024 rows do not
1066
+ * break; the operations-layer multi-write helper (#1010) makes the
1067
+ * field load-bearing at write time per TR-024 rule 2 (write-time
1068
+ * source = canonical Tenant.displayName).
1069
+ * @see TR-024 — Denormalized display-name attributes
286
1070
  */
287
- linkedDataIdentityRef: {
1071
+ denormalizedTenantName: {
1072
+ type: "string",
1073
+ required: false
1074
+ },
1075
+ /**
1076
+ * Denormalized display name of the linked User, captured at row
1077
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1078
+ * adjacency-list canonical-record GSI1SK (pattern #8 —
1079
+ * `<roleId>#<normalizedUserName>#<id>`) and workspace-projection SK
1080
+ * (pattern #9) can be composed from a top-level field. Optional on
1081
+ * the schema so pre-TR-024 rows do not break; the operations-layer
1082
+ * multi-write helper (#1010) makes the field load-bearing at write
1083
+ * time per TR-024 rule 2 (write-time source = canonical
1084
+ * User.displayName).
1085
+ * @see TR-024 — Denormalized display-name attributes
1086
+ */
1087
+ denormalizedUserName: {
1088
+ type: "string",
1089
+ required: false
1090
+ },
1091
+ /**
1092
+ * Denormalized display name of the linked Role, captured at row
1093
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1094
+ * adjacency-list user-projection SK (pattern #5 —
1095
+ * `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#…`) can be composed from
1096
+ * a top-level field. Optional on the schema so pre-TR-024 rows do not
1097
+ * break; the operations-layer multi-write helper (#1010) makes the
1098
+ * field load-bearing at write time per TR-024 rule 2 (write-time
1099
+ * source = canonical Role.displayName).
1100
+ * @see TR-024 — Denormalized display-name attributes
1101
+ */
1102
+ denormalizedRoleName: {
288
1103
  type: "string",
289
1104
  required: false
290
1105
  }
291
1106
  },
292
1107
  indexes: {
293
- /** Base table: PK = TID#<tenantId>#MEMBERSHIP#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
1108
+ /** Base table: PK = TID#<tenantId>#ROLEASSIGNMENT#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
294
1109
  record: {
295
1110
  pk: {
296
1111
  field: "PK",
297
1112
  composite: ["tenantId", "id"],
298
- template: "TID#${tenantId}#MEMBERSHIP#ID#${id}"
1113
+ template: "TID#${tenantId}#ROLEASSIGNMENT#ID#${id}"
299
1114
  },
300
1115
  sk: {
301
1116
  field: "SK",
@@ -304,18 +1119,21 @@ var MembershipEntity = new import_electrodb2.Entity({
304
1119
  }
305
1120
  },
306
1121
  /**
307
- * GSI1 — Unified Sharded List per ADR-011: list all Memberships for a tenant across the
308
- * four shards. Membership is tenant-scoped only, so `WID#-` is a sentinel.
309
- * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
310
- * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
311
- * normalized label and ISO-8601 `T`/`Z`.
1122
+ * GSI1 — Unified Sharded List per ADR-011: list all RoleAssignments for a tenant across the
1123
+ * four shards. Tenant-scoped only, so `WID#-` is a sentinel.
1124
+ * SK is derived via `roleAssignmentGsi1skAttribute` — composes the
1125
+ * discriminator-first `<roleId>#<normalizedUserName>#<id>` shape per
1126
+ * ADR-018 pattern #8 (users with a specific role in a tenant, sorted
1127
+ * by user name); falls back to `<lastUpdated>#<id>` when either
1128
+ * component is missing. `casing: "none"` preserves the normalized
1129
+ * label and ISO-8601 `T`/`Z`.
312
1130
  */
313
1131
  gsi1: {
314
1132
  index: "GSI1",
315
1133
  pk: {
316
1134
  field: "GSI1PK",
317
1135
  composite: ["tenantId", "gsi1Shard"],
318
- template: "TID#${tenantId}#WID#-#RT#Membership#SHARD#${gsi1Shard}"
1136
+ template: "TID#${tenantId}#WID#-#RT#RoleAssignment#SHARD#${gsi1Shard}"
319
1137
  },
320
1138
  sk: {
321
1139
  field: "GSI1SK",
@@ -327,206 +1145,285 @@ var MembershipEntity = new import_electrodb2.Entity({
327
1145
  }
328
1146
  });
329
1147
 
330
- // src/data/dynamo/entities/control/role-entity.ts
331
- var import_electrodb3 = require("electrodb");
332
- var RoleEntity = new import_electrodb3.Entity({
1148
+ // src/data/dynamo/entities/control/roleassignment-user-projection-entity.ts
1149
+ var import_electrodb9 = require("electrodb");
1150
+ var RoleAssignmentUserProjectionEntity = new import_electrodb9.Entity({
333
1151
  model: {
334
- entity: "role",
1152
+ entity: "roleAssignmentUserProjection",
335
1153
  service: "control",
336
1154
  version: "01"
337
1155
  },
338
1156
  attributes: {
339
- /** Sort key sentinel. Always "CURRENT". */
1157
+ /**
1158
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
1159
+ * base-table PK. Always required — the projection has no meaning
1160
+ * outside a user partition.
1161
+ */
1162
+ userId: {
1163
+ type: "string",
1164
+ required: true
1165
+ },
1166
+ /**
1167
+ * Pre-composed sort key — built by the operations-layer projection
1168
+ * writer via `buildRoleAssignmentUserProjectionSk*` helpers. The
1169
+ * entity stores the value verbatim so the SK grammar (tenant-lane
1170
+ * vs workspace-lane) is owned by the operations layer, not
1171
+ * duplicated here.
1172
+ */
340
1173
  sk: {
341
1174
  type: "string",
342
- required: true,
343
- default: "CURRENT"
1175
+ required: true
344
1176
  },
345
- /** FHIR Resource.id; role id. */
346
- id: {
1177
+ /** Tenant in which the role assignment applies. Always required. */
1178
+ tenantId: {
347
1179
  type: "string",
348
1180
  required: true
349
1181
  },
350
- /** Full Role resource serialized as JSON string. */
351
- resource: {
1182
+ /**
1183
+ * Workspace the role assignment scopes to. Present iff the
1184
+ * projection row is the workspace-level sub-lane; absent for
1185
+ * tenant-level sub-lane rows.
1186
+ */
1187
+ workspaceId: {
1188
+ type: "string",
1189
+ required: false
1190
+ },
1191
+ /**
1192
+ * Role the assignment grants. Stored as a discriminating field so
1193
+ * `Query(PK = USER#ID#<userId>, SK begins_with 'ROLEASSIGNMENT#…')`
1194
+ * results carry the role id without a hop to the canonical row.
1195
+ */
1196
+ roleId: {
352
1197
  type: "string",
353
1198
  required: true
354
1199
  },
355
1200
  /**
356
- * Summary projection (key display fields as JSON string: id, displayName, status).
357
- * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
1201
+ * RoleAssignment canonical-record id. Stored as a discriminating
1202
+ * field so consumers can hydrate the canonical row via
1203
+ * `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
1204
+ * when the projection's `summary` is insufficient.
1205
+ */
1206
+ roleAssignmentId: {
1207
+ type: "string",
1208
+ required: true
1209
+ },
1210
+ /**
1211
+ * Summary projection (key display fields as JSON string: id,
1212
+ * displayName, status) — mirrored from the canonical RoleAssignment
1213
+ * row so user-partition queries do not need a BatchGet hop.
358
1214
  */
359
1215
  summary: {
360
1216
  type: "string",
361
1217
  required: true
362
1218
  },
363
- /** Version id (e.g. ULID). */
1219
+ /** Version id mirrored from the canonical RoleAssignment row. */
364
1220
  vid: {
365
1221
  type: "string",
366
1222
  required: true
367
1223
  },
1224
+ /** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
368
1225
  lastUpdated: {
369
1226
  type: "string",
370
1227
  required: true
371
1228
  },
372
- gsi1Shard: gsi1ShardAttribute,
373
- /** Derived GSI1 sort keyname-based when extractable; else `<lastUpdated>#<id>`. */
374
- gsi1sk: gsi1skAttribute,
375
- deleted: {
376
- type: "boolean",
1229
+ /**
1230
+ * Denormalized Tenant display namemirrored from the canonical
1231
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
1232
+ * Optional on the schema because pre-TR-024 rows may not carry a
1233
+ * display name; the operations layer falls back gracefully when
1234
+ * missing.
1235
+ */
1236
+ denormalizedTenantName: {
1237
+ type: "string",
377
1238
  required: false
378
1239
  },
379
- bundleId: {
1240
+ /**
1241
+ * Denormalized User display name — mirrored from the canonical
1242
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
1243
+ * Carried on the projection so consumers can render the user's
1244
+ * display name without a hop to the User record.
1245
+ */
1246
+ denormalizedUserName: {
380
1247
  type: "string",
381
1248
  required: false
382
1249
  },
383
- msgId: {
1250
+ /**
1251
+ * Denormalized Role display name — required to compose the SK's
1252
+ * `<normalizedRoleName>` segment. Optional on the schema (pre-TR-024
1253
+ * rows fall back to a sentinel) but expected to be present at write
1254
+ * time per TR-024 rule 2 (write-time source =
1255
+ * canonical Role.displayName).
1256
+ */
1257
+ denormalizedRoleName: {
384
1258
  type: "string",
385
1259
  required: false
386
1260
  }
387
1261
  },
388
1262
  indexes: {
389
- /** Base table: PK = ROLE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
1263
+ /**
1264
+ * Base table: PK = USER#ID#<userId>, SK = operation-supplied. Both
1265
+ * sub-lanes (tenant-level and workspace-level) use this same index —
1266
+ * the SK string encodes the lane discriminator
1267
+ * (`ROLEASSIGNMENT#TENANT#…` vs `ROLEASSIGNMENT#WORKSPACE#…`) so a
1268
+ * single `Query(PK = USER#ID#<userId>, SK begins_with
1269
+ * 'ROLEASSIGNMENT#')` returns both lanes interleaved.
1270
+ */
390
1271
  record: {
391
1272
  pk: {
392
1273
  field: "PK",
393
- composite: ["id"],
394
- template: "ROLE#ID#${id}"
1274
+ composite: ["userId"],
1275
+ template: "USER#ID#${userId}"
395
1276
  },
396
1277
  sk: {
397
1278
  field: "SK",
1279
+ casing: "none",
398
1280
  composite: ["sk"],
399
1281
  template: "${sk}"
400
1282
  }
401
- },
402
- /**
403
- * GSI1 — Unified Sharded List per ADR-011: list all Roles across the four shards.
404
- * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#Role#SHARD#<n>`.
405
- * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
406
- * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
407
- * normalized label and ISO-8601 `T`/`Z`.
408
- */
409
- gsi1: {
410
- index: "GSI1",
411
- pk: {
412
- field: "GSI1PK",
413
- composite: ["gsi1Shard"],
414
- template: "TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}"
415
- },
416
- sk: {
417
- field: "GSI1SK",
418
- casing: "none",
419
- composite: ["gsi1sk"],
420
- template: "${gsi1sk}"
421
- }
422
1283
  }
423
1284
  }
424
1285
  });
425
1286
 
426
- // src/data/dynamo/entities/control/roleassignment-entity.ts
427
- var import_electrodb4 = require("electrodb");
428
- var RoleAssignmentEntity = new import_electrodb4.Entity({
1287
+ // src/data/dynamo/entities/control/roleassignment-workspace-projection-entity.ts
1288
+ var import_electrodb10 = require("electrodb");
1289
+ var RoleAssignmentWorkspaceProjectionEntity = new import_electrodb10.Entity({
429
1290
  model: {
430
- entity: "roleassignment",
1291
+ entity: "roleAssignmentWorkspaceProjection",
431
1292
  service: "control",
432
1293
  version: "01"
433
1294
  },
434
1295
  attributes: {
435
- /** Sort key sentinel. Always "CURRENT". */
1296
+ /**
1297
+ * Tenant the workspace belongs to. Renders as the leading segment
1298
+ * of the base-table PK. Always required — the workspace partition
1299
+ * is tenant-scoped per ADR-011.
1300
+ */
1301
+ tenantId: {
1302
+ type: "string",
1303
+ required: true
1304
+ },
1305
+ /**
1306
+ * Workspace partition discriminator. Renders as the trailing
1307
+ * segment of the base-table PK
1308
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
1309
+ * the projection has no meaning outside a workspace partition.
1310
+ */
1311
+ workspaceId: {
1312
+ type: "string",
1313
+ required: true
1314
+ },
1315
+ /**
1316
+ * Pre-composed sort key — built by the operations-layer projection
1317
+ * writer via `buildRoleAssignmentWorkspaceProjectionSk`. The entity
1318
+ * stores the value verbatim so the SK grammar (pattern #9) is
1319
+ * owned by the operations layer, not duplicated here.
1320
+ */
436
1321
  sk: {
437
1322
  type: "string",
438
- required: true,
439
- default: "CURRENT"
1323
+ required: true
440
1324
  },
441
- /** Tenant in which the role assignment applies (required). */
442
- tenantId: {
1325
+ /**
1326
+ * User the role assignment grants the role to. Stored as a
1327
+ * discriminating field so consumers can hydrate the canonical User
1328
+ * row via `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
1329
+ * projection's `summary` is insufficient.
1330
+ */
1331
+ userId: {
443
1332
  type: "string",
444
1333
  required: true
445
1334
  },
446
- /** FHIR Resource.id; role assignment id. */
447
- id: {
1335
+ /**
1336
+ * Role the assignment grants. Stored as a discriminating field —
1337
+ * also rendered into the SK as the discriminator-first segment so
1338
+ * `begins_with('ROLEASSIGNMENT#<roleId>#')` filters one role.
1339
+ */
1340
+ roleId: {
448
1341
  type: "string",
449
1342
  required: true
450
1343
  },
451
- /** Full RoleAssignment resource serialized as JSON string. */
452
- resource: {
1344
+ /**
1345
+ * RoleAssignment canonical-record id. Stored as a discriminating
1346
+ * field so consumers can hydrate the canonical row via
1347
+ * `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
1348
+ * when the projection's `summary` is insufficient.
1349
+ */
1350
+ roleAssignmentId: {
453
1351
  type: "string",
454
1352
  required: true
455
1353
  },
456
1354
  /**
457
- * Summary projection (key display fields as JSON string: id, displayName, status).
458
- * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
1355
+ * Summary projection (key display fields as JSON string: id,
1356
+ * displayName, status) mirrored from the canonical RoleAssignment
1357
+ * row so workspace-partition queries do not need a BatchGet hop.
459
1358
  */
460
1359
  summary: {
461
1360
  type: "string",
462
1361
  required: true
463
1362
  },
464
- /** Version id (e.g. ULID). */
1363
+ /** Version id mirrored from the canonical RoleAssignment row. */
465
1364
  vid: {
466
1365
  type: "string",
467
1366
  required: true
468
1367
  },
1368
+ /** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
469
1369
  lastUpdated: {
470
1370
  type: "string",
471
1371
  required: true
472
1372
  },
473
- gsi1Shard: gsi1ShardAttribute,
474
- /** Derived GSI1 sort keyname-based when extractable; else `<lastUpdated>#<id>`. */
475
- gsi1sk: gsi1skAttribute,
476
- deleted: {
477
- type: "boolean",
478
- required: false
479
- },
480
- bundleId: {
1373
+ /**
1374
+ * Denormalized User display namerequired to compose the
1375
+ * pattern-#9 SK (`ROLEASSIGNMENT#<roleId>#<normalizedUserName>#…`).
1376
+ * Optional on the schema because pre-TR-024 rows may not carry a
1377
+ * display name; the operations layer falls back to a sentinel when
1378
+ * missing so the SK still has a valid shape. The TR-023 rename-
1379
+ * cascade pipeline rewrites the SK on a User rename.
1380
+ */
1381
+ denormalizedUserName: {
481
1382
  type: "string",
482
1383
  required: false
483
1384
  },
484
- msgId: {
1385
+ /**
1386
+ * Denormalized Role display name — mirrored from the canonical
1387
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
1388
+ * Carried on the projection so consumers can render the role's
1389
+ * display name without a hop to the Role record. Not part of the
1390
+ * SK (pattern #9 sorts on `<normalizedUserName>`, not role name) —
1391
+ * a Role rename does NOT rewrite this SK.
1392
+ */
1393
+ denormalizedRoleName: {
485
1394
  type: "string",
486
1395
  required: false
487
1396
  }
488
1397
  },
489
1398
  indexes: {
490
- /** Base table: PK = TID#<tenantId>#ROLEASSIGNMENT#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
1399
+ /**
1400
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
1401
+ * SK = operation-supplied. Pattern #9 uses this index — the SK
1402
+ * encodes the entity-type prefix and discriminator-first roleId
1403
+ * (`ROLEASSIGNMENT#<roleId>#…`) so
1404
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'ROLEASSIGNMENT#<roleId>#')`
1405
+ * returns every user-assignment for that role in the workspace, sorted
1406
+ * by normalized user name.
1407
+ */
491
1408
  record: {
492
1409
  pk: {
493
1410
  field: "PK",
494
- composite: ["tenantId", "id"],
495
- template: "TID#${tenantId}#ROLEASSIGNMENT#ID#${id}"
1411
+ composite: ["tenantId", "workspaceId"],
1412
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
496
1413
  },
497
1414
  sk: {
498
1415
  field: "SK",
1416
+ casing: "none",
499
1417
  composite: ["sk"],
500
1418
  template: "${sk}"
501
1419
  }
502
- },
503
- /**
504
- * GSI1 — Unified Sharded List per ADR-011: list all RoleAssignments for a tenant across the
505
- * four shards. Tenant-scoped only, so `WID#-` is a sentinel.
506
- * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
507
- * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
508
- * normalized label and ISO-8601 `T`/`Z`.
509
- */
510
- gsi1: {
511
- index: "GSI1",
512
- pk: {
513
- field: "GSI1PK",
514
- composite: ["tenantId", "gsi1Shard"],
515
- template: "TID#${tenantId}#WID#-#RT#RoleAssignment#SHARD#${gsi1Shard}"
516
- },
517
- sk: {
518
- field: "GSI1SK",
519
- casing: "none",
520
- composite: ["gsi1sk"],
521
- template: "${gsi1sk}"
522
- }
523
1420
  }
524
1421
  }
525
1422
  });
526
1423
 
527
1424
  // src/data/dynamo/entities/control/tenant-entity.ts
528
- var import_electrodb5 = require("electrodb");
529
- var TenantEntity = new import_electrodb5.Entity({
1425
+ var import_electrodb11 = require("electrodb");
1426
+ var TenantEntity = new import_electrodb11.Entity({
530
1427
  model: {
531
1428
  entity: "tenant",
532
1429
  service: "control",
@@ -626,8 +1523,8 @@ var TenantEntity = new import_electrodb5.Entity({
626
1523
  });
627
1524
 
628
1525
  // src/data/dynamo/entities/control/user-entity.ts
629
- var import_electrodb6 = require("electrodb");
630
- var UserEntity = new import_electrodb6.Entity({
1526
+ var import_electrodb12 = require("electrodb");
1527
+ var UserEntity = new import_electrodb12.Entity({
631
1528
  model: {
632
1529
  entity: "user",
633
1530
  service: "control",
@@ -682,6 +1579,28 @@ var UserEntity = new import_electrodb6.Entity({
682
1579
  type: "boolean",
683
1580
  required: false
684
1581
  },
1582
+ /**
1583
+ * TR-022 / ADR-018 lifecycle state for the cascade pipeline.
1584
+ *
1585
+ * - `active` (or undefined) — normal, readable state.
1586
+ * - `deleting` — intermediate state set synchronously by the
1587
+ * hard-delete API entry point. The owning-delete cascade state
1588
+ * machine fans out from this transition (DynamoDB stream →
1589
+ * `control-plane.owning-delete.v1` → Step Functions). Readers MUST
1590
+ * short-circuit on `deleting` so partial cascades stay invisible.
1591
+ * - `deleted-failed` — terminal failure state set by the cascade
1592
+ * finalize Lambda when the cascade run fails irrecoverably.
1593
+ * Operators recover by re-running the cascade or by direct
1594
+ * intervention.
1595
+ *
1596
+ * The cascade finalize step deletes the canonical record conditional
1597
+ * on `lifecycleState = "deleting"`; on replay the conditional check
1598
+ * fails and the finalize step treats that as a no-op success.
1599
+ */
1600
+ lifecycleState: {
1601
+ type: ["active", "deleting", "deleted-failed"],
1602
+ required: false
1603
+ },
685
1604
  bundleId: {
686
1605
  type: "string",
687
1606
  required: false
@@ -751,8 +1670,8 @@ var UserEntity = new import_electrodb6.Entity({
751
1670
  });
752
1671
 
753
1672
  // src/data/dynamo/entities/control/workspace-entity.ts
754
- var import_electrodb7 = require("electrodb");
755
- var WorkspaceEntity = new import_electrodb7.Entity({
1673
+ var import_electrodb13 = require("electrodb");
1674
+ var WorkspaceEntity = new import_electrodb13.Entity({
756
1675
  model: {
757
1676
  entity: "workspace",
758
1677
  service: "control",
@@ -804,6 +1723,28 @@ var WorkspaceEntity = new import_electrodb7.Entity({
804
1723
  type: "boolean",
805
1724
  required: false
806
1725
  },
1726
+ /**
1727
+ * TR-022 / ADR-018 lifecycle state for the cascade pipeline.
1728
+ *
1729
+ * - `active` (or undefined) — normal, readable state.
1730
+ * - `deleting` — intermediate state set synchronously by the
1731
+ * hard-delete API entry point. The owning-delete cascade state
1732
+ * machine fans out from this transition (DynamoDB stream →
1733
+ * `control-plane.owning-delete.v1` → Step Functions). Readers MUST
1734
+ * short-circuit on `deleting` so partial cascades stay invisible.
1735
+ * - `deleted-failed` — terminal failure state set by the cascade
1736
+ * finalize Lambda when the cascade run fails irrecoverably.
1737
+ * Operators recover by re-running the cascade or by direct
1738
+ * intervention.
1739
+ *
1740
+ * The cascade finalize step deletes the canonical record conditional
1741
+ * on `lifecycleState = "deleting"`; on replay the conditional check
1742
+ * fails and the finalize step treats that as a no-op success.
1743
+ */
1744
+ lifecycleState: {
1745
+ type: ["active", "deleting", "deleted-failed"],
1746
+ required: false
1747
+ },
807
1748
  bundleId: {
808
1749
  type: "string",
809
1750
  required: false
@@ -854,28 +1795,36 @@ var WorkspaceEntity = new import_electrodb7.Entity({
854
1795
  // src/data/dynamo/dynamo-control-service.ts
855
1796
  var controlPlaneEntities = {
856
1797
  configuration: ConfigurationEntity,
1798
+ configurationUserProjection: ConfigurationUserProjectionEntity,
1799
+ configurationWorkspaceProjection: ConfigurationWorkspaceProjectionEntity,
857
1800
  membership: MembershipEntity,
1801
+ membershipUserProjection: MembershipUserProjectionEntity,
1802
+ membershipWorkspaceProjection: MembershipWorkspaceProjectionEntity,
858
1803
  role: RoleEntity,
859
1804
  roleAssignment: RoleAssignmentEntity,
1805
+ roleAssignmentUserProjection: RoleAssignmentUserProjectionEntity,
1806
+ roleAssignmentWorkspaceProjection: RoleAssignmentWorkspaceProjectionEntity,
860
1807
  tenant: TenantEntity,
861
1808
  user: UserEntity,
862
1809
  workspace: WorkspaceEntity
863
1810
  };
864
- var controlPlaneService = new import_electrodb8.Service(controlPlaneEntities, {
1811
+ var controlPlaneService = new import_electrodb14.Service(controlPlaneEntities, {
865
1812
  table: defaultTableName,
866
1813
  client: dynamoClient
867
1814
  });
868
1815
  var DynamoControlService = {
869
- entities: controlPlaneService.entities
1816
+ entities: controlPlaneService.entities,
1817
+ transaction: controlPlaneService.transaction
870
1818
  };
871
1819
  function getDynamoControlService(tableName) {
872
1820
  const resolved = tableName ?? defaultTableName;
873
- const service = new import_electrodb8.Service(controlPlaneEntities, {
1821
+ const service = new import_electrodb14.Service(controlPlaneEntities, {
874
1822
  table: resolved,
875
1823
  client: dynamoClient
876
1824
  });
877
1825
  return {
878
- entities: service.entities
1826
+ entities: service.entities,
1827
+ transaction: service.transaction
879
1828
  };
880
1829
  }
881
1830
 
@@ -1132,10 +2081,10 @@ function idFromReference(reference, prefix) {
1132
2081
  var import_types5 = require("@openhi/types");
1133
2082
 
1134
2083
  // src/data/dynamo/dynamo-data-service.ts
1135
- var import_electrodb10 = require("electrodb");
2084
+ var import_electrodb16 = require("electrodb");
1136
2085
 
1137
2086
  // src/data/dynamo/entities/data-entity-common.ts
1138
- var import_electrodb9 = require("electrodb");
2087
+ var import_electrodb15 = require("electrodb");
1139
2088
  var dataEntityAttributes = {
1140
2089
  /** Sort key. "CURRENT" for current version; version history in S3. */
1141
2090
  sk: {
@@ -1231,7 +2180,7 @@ var dataEntityAttributes = {
1231
2180
  }
1232
2181
  };
1233
2182
  function createDataEntity(entity, resourceTypeLabel) {
1234
- return new import_electrodb9.Entity({
2183
+ return new import_electrodb15.Entity({
1235
2184
  model: {
1236
2185
  entity,
1237
2186
  service: "data",
@@ -2145,21 +3094,23 @@ var dataPlaneEntities = {
2145
3094
  visionprescription: VisionPrescriptionEntity,
2146
3095
  verificationresult: VerificationResultEntity
2147
3096
  };
2148
- var dataPlaneService = new import_electrodb10.Service(dataPlaneEntities, {
3097
+ var dataPlaneService = new import_electrodb16.Service(dataPlaneEntities, {
2149
3098
  table: defaultTableName,
2150
3099
  client: dynamoClient
2151
3100
  });
2152
3101
  var DynamoDataService = {
2153
- entities: dataPlaneService.entities
3102
+ entities: dataPlaneService.entities,
3103
+ transaction: dataPlaneService.transaction
2154
3104
  };
2155
3105
  function getDynamoDataService(tableName) {
2156
3106
  const resolved = tableName ?? defaultTableName;
2157
- const service = new import_electrodb10.Service(dataPlaneEntities, {
3107
+ const service = new import_electrodb16.Service(dataPlaneEntities, {
2158
3108
  table: resolved,
2159
3109
  client: dynamoClient
2160
3110
  });
2161
3111
  return {
2162
- entities: service.entities
3112
+ entities: service.entities,
3113
+ transaction: service.transaction
2163
3114
  };
2164
3115
  }
2165
3116