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