@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,2716 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __commonJS = (cb, mod) => function __require() {
9
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+
33
+ // ../workflows/lib/envelope-version.js
34
+ var require_envelope_version = __commonJS({
35
+ "../workflows/lib/envelope-version.js"(exports2) {
36
+ "use strict";
37
+ Object.defineProperty(exports2, "__esModule", { value: true });
38
+ exports2.ENVELOPE_VERSION = void 0;
39
+ exports2.isSupportedEnvelopeVersion = isSupportedEnvelopeVersion;
40
+ exports2.ENVELOPE_VERSION = "1.0";
41
+ var ENVELOPE_VERSION_PATTERN = /^\d+\.\d+$/;
42
+ var MIN_SUPPORTED_MAJOR = 1;
43
+ var MAX_SUPPORTED_MAJOR = 1;
44
+ function isSupportedEnvelopeVersion(version) {
45
+ if (!ENVELOPE_VERSION_PATTERN.test(version)) {
46
+ return false;
47
+ }
48
+ const major = Number.parseInt(version.split(".")[0], 10);
49
+ return major >= MIN_SUPPORTED_MAJOR && major <= MAX_SUPPORTED_MAJOR;
50
+ }
51
+ }
52
+ });
53
+
54
+ // ../workflows/lib/envelope.js
55
+ var require_envelope = __commonJS({
56
+ "../workflows/lib/envelope.js"(exports2) {
57
+ "use strict";
58
+ Object.defineProperty(exports2, "__esModule", { value: true });
59
+ exports2.MissingActorContextError = void 0;
60
+ exports2.isWorkflowUserActor = isWorkflowUserActor;
61
+ exports2.isWorkflowSystemActor = isWorkflowSystemActor;
62
+ exports2.workflowUserActorFromClaims = workflowUserActorFromClaims;
63
+ function isWorkflowUserActor(actor) {
64
+ return actor.ohi_uid !== void 0;
65
+ }
66
+ function isWorkflowSystemActor(actor) {
67
+ return actor.system !== void 0;
68
+ }
69
+ function workflowUserActorFromClaims(claims) {
70
+ if (claims.ohi_tid === void 0 || claims.ohi_wid === void 0) {
71
+ throw new MissingActorContextError("workflowUserActorFromClaims: ohi_tid and ohi_wid are required on the workflow user-actor; the caller's JWT is missing one or both. Use a system-actor for pre-provisioning bootstrap workflows.");
72
+ }
73
+ return {
74
+ ohi_tid: claims.ohi_tid,
75
+ ohi_wid: claims.ohi_wid,
76
+ ohi_uid: claims.ohi_uid,
77
+ ohi_uname: claims.ohi_uname
78
+ };
79
+ }
80
+ var MissingActorContextError = class extends Error {
81
+ /** @param message - human-readable description of the missing claim. */
82
+ constructor(message) {
83
+ super(message);
84
+ this.name = "MissingActorContextError";
85
+ }
86
+ };
87
+ exports2.MissingActorContextError = MissingActorContextError;
88
+ }
89
+ });
90
+
91
+ // ../workflows/lib/sources.js
92
+ var require_sources = __commonJS({
93
+ "../workflows/lib/sources.js"(exports2) {
94
+ "use strict";
95
+ Object.defineProperty(exports2, "__esModule", { value: true });
96
+ exports2.DEFAULT_BUS_NAME_BY_SOURCE = exports2.OPENHI_OPS_SOURCE = exports2.OPENHI_DATA_SOURCE = exports2.OPENHI_CONTROL_SOURCE = void 0;
97
+ exports2.OPENHI_CONTROL_SOURCE = "openhi.control";
98
+ exports2.OPENHI_DATA_SOURCE = "openhi.data";
99
+ exports2.OPENHI_OPS_SOURCE = "openhi.ops";
100
+ exports2.DEFAULT_BUS_NAME_BY_SOURCE = {
101
+ [exports2.OPENHI_CONTROL_SOURCE]: "openhi-control-event-bus",
102
+ [exports2.OPENHI_DATA_SOURCE]: "openhi-data-event-bus",
103
+ [exports2.OPENHI_OPS_SOURCE]: "openhi-ops-event-bus"
104
+ };
105
+ }
106
+ });
107
+
108
+ // ../workflows/lib/detail-types/registry.js
109
+ var require_registry = __commonJS({
110
+ "../workflows/lib/detail-types/registry.js"(exports2) {
111
+ "use strict";
112
+ Object.defineProperty(exports2, "__esModule", { value: true });
113
+ exports2.InvalidDetailTypeRegistrationError = void 0;
114
+ exports2.defineDetailType = defineDetailType;
115
+ exports2.isWellFormedDetailType = isWellFormedDetailType;
116
+ function defineDetailType(entry) {
117
+ if (!isWellFormedDetailType(entry.detailType)) {
118
+ throw new InvalidDetailTypeRegistrationError(`Detail-type "${entry.detailType}" does not match the platform-wide format <area>.<event>.v<integer>. See TR-016 \xA7Open Items #2.`);
119
+ }
120
+ return entry;
121
+ }
122
+ var DETAIL_TYPE_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*\.[a-z0-9]+(?:-[a-z0-9]+)*\.v\d+$/;
123
+ function isWellFormedDetailType(detailType) {
124
+ return DETAIL_TYPE_PATTERN.test(detailType);
125
+ }
126
+ var InvalidDetailTypeRegistrationError = class extends Error {
127
+ /** @param message - human-readable description of the violation. */
128
+ constructor(message) {
129
+ super(message);
130
+ this.name = "InvalidDetailTypeRegistrationError";
131
+ }
132
+ };
133
+ exports2.InvalidDetailTypeRegistrationError = InvalidDetailTypeRegistrationError;
134
+ }
135
+ });
136
+
137
+ // ../workflows/lib/detail-types/control-plane.js
138
+ var require_control_plane = __commonJS({
139
+ "../workflows/lib/detail-types/control-plane.js"(exports2) {
140
+ "use strict";
141
+ Object.defineProperty(exports2, "__esModule", { value: true });
142
+ exports2.ControlPlaneRenameFailedV1 = exports2.ControlPlaneRenameCompleteV1 = exports2.ControlPlaneRenameV1 = exports2.RENAMABLE_ENTITY_TYPE = exports2.ControlPlaneOwningDeleteFailedV1 = exports2.ControlPlaneOwningDeleteCompleteV1 = exports2.ControlPlaneOwningDeleteV1 = exports2.OWNING_ENTITY_TYPE = void 0;
143
+ var sources_1 = require_sources();
144
+ var registry_1 = require_registry();
145
+ exports2.OWNING_ENTITY_TYPE = {
146
+ Workspace: "Workspace",
147
+ User: "User"
148
+ };
149
+ exports2.ControlPlaneOwningDeleteV1 = (0, registry_1.defineDetailType)({
150
+ detailType: "control-plane.owning-delete.v1",
151
+ source: sources_1.OPENHI_DATA_SOURCE,
152
+ dedupRequired: true
153
+ });
154
+ exports2.ControlPlaneOwningDeleteCompleteV1 = (0, registry_1.defineDetailType)({
155
+ detailType: "control-plane.owning-delete-complete.v1",
156
+ source: sources_1.OPENHI_OPS_SOURCE,
157
+ dedupRequired: true
158
+ });
159
+ exports2.ControlPlaneOwningDeleteFailedV1 = (0, registry_1.defineDetailType)({
160
+ detailType: "control-plane.owning-delete-failed.v1",
161
+ source: sources_1.OPENHI_OPS_SOURCE,
162
+ dedupRequired: true
163
+ });
164
+ exports2.RENAMABLE_ENTITY_TYPE = {
165
+ Tenant: "Tenant",
166
+ User: "User",
167
+ Role: "Role"
168
+ };
169
+ exports2.ControlPlaneRenameV1 = (0, registry_1.defineDetailType)({
170
+ detailType: "control-plane.rename.v1",
171
+ source: sources_1.OPENHI_DATA_SOURCE,
172
+ dedupRequired: true
173
+ });
174
+ exports2.ControlPlaneRenameCompleteV1 = (0, registry_1.defineDetailType)({
175
+ detailType: "control-plane.rename-complete.v1",
176
+ source: sources_1.OPENHI_OPS_SOURCE,
177
+ dedupRequired: true
178
+ });
179
+ exports2.ControlPlaneRenameFailedV1 = (0, registry_1.defineDetailType)({
180
+ detailType: "control-plane.rename-failed.v1",
181
+ source: sources_1.OPENHI_OPS_SOURCE,
182
+ dedupRequired: true
183
+ });
184
+ }
185
+ });
186
+
187
+ // ../workflows/lib/detail-types/platform.js
188
+ var require_platform = __commonJS({
189
+ "../workflows/lib/detail-types/platform.js"(exports2) {
190
+ "use strict";
191
+ Object.defineProperty(exports2, "__esModule", { value: true });
192
+ exports2.PlatformSystemDataSeededV1 = exports2.PlatformDeploymentCompletedV1 = void 0;
193
+ var sources_1 = require_sources();
194
+ var registry_1 = require_registry();
195
+ exports2.PlatformDeploymentCompletedV1 = (0, registry_1.defineDetailType)({
196
+ detailType: "platform.deployment-completed.v1",
197
+ source: sources_1.OPENHI_CONTROL_SOURCE,
198
+ dedupRequired: true
199
+ });
200
+ exports2.PlatformSystemDataSeededV1 = (0, registry_1.defineDetailType)({
201
+ detailType: "platform.system-data-seeded.v1",
202
+ source: sources_1.OPENHI_CONTROL_SOURCE,
203
+ dedupRequired: true
204
+ });
205
+ }
206
+ });
207
+
208
+ // ../workflows/lib/detail-types/index.js
209
+ var require_detail_types = __commonJS({
210
+ "../workflows/lib/detail-types/index.js"(exports2) {
211
+ "use strict";
212
+ var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
213
+ if (k2 === void 0) k2 = k;
214
+ var desc = Object.getOwnPropertyDescriptor(m, k);
215
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
216
+ desc = { enumerable: true, get: function() {
217
+ return m[k];
218
+ } };
219
+ }
220
+ Object.defineProperty(o, k2, desc);
221
+ }) : (function(o, m, k, k2) {
222
+ if (k2 === void 0) k2 = k;
223
+ o[k2] = m[k];
224
+ }));
225
+ var __exportStar = exports2 && exports2.__exportStar || function(m, exports3) {
226
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
227
+ };
228
+ Object.defineProperty(exports2, "__esModule", { value: true });
229
+ __exportStar(require_control_plane(), exports2);
230
+ __exportStar(require_platform(), exports2);
231
+ __exportStar(require_registry(), exports2);
232
+ }
233
+ });
234
+
235
+ // ../workflows/lib/publisher.js
236
+ var require_publisher = __commonJS({
237
+ "../workflows/lib/publisher.js"(exports2) {
238
+ "use strict";
239
+ Object.defineProperty(exports2, "__esModule", { value: true });
240
+ exports2.WorkflowPublishError = void 0;
241
+ exports2.workflowsClient = workflowsClient;
242
+ exports2.publishWorkflowEvent = publishWorkflowEvent;
243
+ var node_crypto_1 = require("crypto");
244
+ var client_eventbridge_1 = require("@aws-sdk/client-eventbridge");
245
+ var envelope_version_1 = require_envelope_version();
246
+ var sources_1 = require_sources();
247
+ function workflowsClient(bridge, options = {}) {
248
+ return {
249
+ publish: (entry, payload, ctx) => publishWorkflowEvent(bridge, entry, payload, ctx, options)
250
+ };
251
+ }
252
+ async function publishWorkflowEvent(bridge, entry, payload, ctx, options = {}) {
253
+ const eventIdGenerator = options.eventIdGenerator ?? (() => (0, node_crypto_1.randomUUID)());
254
+ const correlationIdGenerator = options.correlationIdGenerator ?? (() => (0, node_crypto_1.randomUUID)());
255
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
256
+ const envelope = {
257
+ eventId: eventIdGenerator(),
258
+ attempt: 1,
259
+ correlationId: ctx.correlationId ?? correlationIdGenerator(),
260
+ causationId: ctx.causationId ?? null,
261
+ actor: ctx.actor,
262
+ occurredAt: now().toISOString(),
263
+ envelopeVersion: envelope_version_1.ENVELOPE_VERSION,
264
+ payload
265
+ };
266
+ const busName = options.busNameByPlane?.[entry.source] ?? sources_1.DEFAULT_BUS_NAME_BY_SOURCE[entry.source];
267
+ const result = await bridge.send(new client_eventbridge_1.PutEventsCommand({
268
+ Entries: [
269
+ {
270
+ EventBusName: busName,
271
+ Source: entry.source,
272
+ DetailType: entry.detailType,
273
+ Detail: JSON.stringify(envelope)
274
+ }
275
+ ]
276
+ }));
277
+ if ((result.FailedEntryCount ?? 0) > 0) {
278
+ const first = result.Entries?.[0];
279
+ throw new WorkflowPublishError(`EventBridge rejected ${entry.detailType} publish on bus ${busName}: ${first?.ErrorCode ?? "unknown"} \u2014 ${first?.ErrorMessage ?? "no error message"}`);
280
+ }
281
+ return { eventId: envelope.eventId };
282
+ }
283
+ var WorkflowPublishError = class extends Error {
284
+ /** @param message - human-readable description of the failed publish. */
285
+ constructor(message) {
286
+ super(message);
287
+ this.name = "WorkflowPublishError";
288
+ }
289
+ };
290
+ exports2.WorkflowPublishError = WorkflowPublishError;
291
+ }
292
+ });
293
+
294
+ // ../workflows/lib/consumer.js
295
+ var require_consumer = __commonJS({
296
+ "../workflows/lib/consumer.js"(exports2) {
297
+ "use strict";
298
+ Object.defineProperty(exports2, "__esModule", { value: true });
299
+ exports2.UnsupportedEnvelopeVersionError = exports2.InvalidWorkflowEventError = void 0;
300
+ exports2.parseWorkflowEvent = parseWorkflowEvent;
301
+ var envelope_version_1 = require_envelope_version();
302
+ function parseWorkflowEvent(event, expected) {
303
+ if (event.source !== expected.source) {
304
+ throw new InvalidWorkflowEventError(`EventBridge source "${event.source}" does not match expected detail-type's source "${expected.source}".`);
305
+ }
306
+ if (event["detail-type"] !== expected.detailType) {
307
+ throw new InvalidWorkflowEventError(`EventBridge detail-type "${event["detail-type"]}" does not match expected "${expected.detailType}".`);
308
+ }
309
+ const candidate = asEnvelopeCandidate(event.detail);
310
+ if (!(0, envelope_version_1.isSupportedEnvelopeVersion)(candidate.envelopeVersion)) {
311
+ throw new UnsupportedEnvelopeVersionError(`Envelope version "${candidate.envelopeVersion}" is outside the SDK's supported range.`);
312
+ }
313
+ const envelope = {
314
+ eventId: candidate.eventId,
315
+ attempt: candidate.attempt,
316
+ correlationId: candidate.correlationId,
317
+ causationId: candidate.causationId,
318
+ actor: candidate.actor,
319
+ occurredAt: candidate.occurredAt,
320
+ envelopeVersion: candidate.envelopeVersion,
321
+ payload: candidate.payload
322
+ };
323
+ return {
324
+ envelope,
325
+ dedupKey: { eventId: envelope.eventId, attempt: envelope.attempt }
326
+ };
327
+ }
328
+ function asEnvelopeCandidate(detail) {
329
+ if (detail === null || typeof detail !== "object") {
330
+ throw new InvalidWorkflowEventError("EventBridge detail is not a non-null object.");
331
+ }
332
+ const obj = detail;
333
+ assertString(obj, "eventId");
334
+ assertPositiveInteger(obj, "attempt");
335
+ assertString(obj, "correlationId");
336
+ assertCausationId(obj);
337
+ assertActor(obj);
338
+ assertString(obj, "occurredAt");
339
+ assertString(obj, "envelopeVersion");
340
+ if (!("payload" in obj)) {
341
+ throw new InvalidWorkflowEventError("Envelope is missing required field: payload.");
342
+ }
343
+ return obj;
344
+ }
345
+ function assertString(obj, field) {
346
+ const value = obj[field];
347
+ if (typeof value !== "string" || value.length === 0) {
348
+ throw new InvalidWorkflowEventError(`Envelope field "${field}" must be a non-empty string.`);
349
+ }
350
+ }
351
+ function assertPositiveInteger(obj, field) {
352
+ const value = obj[field];
353
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
354
+ throw new InvalidWorkflowEventError(`Envelope field "${field}" must be a 1-indexed integer.`);
355
+ }
356
+ }
357
+ function assertCausationId(obj) {
358
+ if (!("causationId" in obj)) {
359
+ throw new InvalidWorkflowEventError("Envelope is missing required field: causationId.");
360
+ }
361
+ const value = obj.causationId;
362
+ if (value !== null && (typeof value !== "string" || value.length === 0)) {
363
+ throw new InvalidWorkflowEventError('Envelope field "causationId" must be a non-empty string or null.');
364
+ }
365
+ }
366
+ function assertActor(obj) {
367
+ const actor = obj.actor;
368
+ if (actor === null || typeof actor !== "object") {
369
+ throw new InvalidWorkflowEventError('Envelope field "actor" must be an object.');
370
+ }
371
+ const actorObj = actor;
372
+ const isUserActor = typeof actorObj.ohi_uid === "string" && typeof actorObj.ohi_uname === "string" && typeof actorObj.ohi_tid === "string" && typeof actorObj.ohi_wid === "string";
373
+ const isSystemActor = typeof actorObj.system === "string";
374
+ if (!isUserActor && !isSystemActor) {
375
+ throw new InvalidWorkflowEventError('Envelope field "actor" must be either a user-actor (ohi_tid, ohi_wid, ohi_uid, ohi_uname) or a system-actor ({ system: string }).');
376
+ }
377
+ }
378
+ var InvalidWorkflowEventError = class extends Error {
379
+ /** @param message - human-readable description of the validation failure. */
380
+ constructor(message) {
381
+ super(message);
382
+ this.name = "InvalidWorkflowEventError";
383
+ }
384
+ };
385
+ exports2.InvalidWorkflowEventError = InvalidWorkflowEventError;
386
+ var UnsupportedEnvelopeVersionError = class extends Error {
387
+ /** @param message - human-readable description of the unsupported version. */
388
+ constructor(message) {
389
+ super(message);
390
+ this.name = "UnsupportedEnvelopeVersionError";
391
+ }
392
+ };
393
+ exports2.UnsupportedEnvelopeVersionError = UnsupportedEnvelopeVersionError;
394
+ }
395
+ });
396
+
397
+ // ../workflows/lib/dedup/env.js
398
+ var require_env = __commonJS({
399
+ "../workflows/lib/dedup/env.js"(exports2) {
400
+ "use strict";
401
+ Object.defineProperty(exports2, "__esModule", { value: true });
402
+ exports2.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH = exports2.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS = exports2.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR = void 0;
403
+ exports2.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR = "OPENHI_WORKFLOW_DEDUP_TABLE_NAME";
404
+ exports2.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS = 14 * 24 * 60 * 60;
405
+ exports2.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH = 64;
406
+ }
407
+ });
408
+
409
+ // ../workflows/lib/dedup/workflow-dedup-client.js
410
+ var require_workflow_dedup_client = __commonJS({
411
+ "../workflows/lib/dedup/workflow-dedup-client.js"(exports2) {
412
+ "use strict";
413
+ Object.defineProperty(exports2, "__esModule", { value: true });
414
+ exports2.WorkflowDedupInvalidInputError = exports2.WorkflowDedupTableNameMissingError = void 0;
415
+ exports2.workflowDedupClient = workflowDedupClient;
416
+ exports2.recordIfAbsent = recordIfAbsent;
417
+ exports2.markFailed = markFailed;
418
+ exports2.encodeSortKey = encodeSortKey;
419
+ var client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
420
+ var env_1 = require_env();
421
+ function workflowDedupClient(dynamodb, options = {}) {
422
+ return {
423
+ recordIfAbsent: (input) => recordIfAbsent(dynamodb, input, options),
424
+ markFailed: (input) => markFailed(dynamodb, input, options)
425
+ };
426
+ }
427
+ async function recordIfAbsent(dynamodb, input, options = {}) {
428
+ assertConsumerName(input.consumerName);
429
+ assertPositiveInteger(input.attempt, "attempt");
430
+ const ttlSeconds = input.ttlSeconds ?? options.defaultTtlSeconds ?? env_1.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS;
431
+ if (!Number.isInteger(ttlSeconds) || ttlSeconds <= 0) {
432
+ throw new WorkflowDedupInvalidInputError(`ttlSeconds must be a positive integer; got ${ttlSeconds}.`);
433
+ }
434
+ const tableName = resolveTableName(options.tableName);
435
+ const now = (options.now ?? defaultNow)();
436
+ const sk = encodeSortKey(input.eventId, input.attempt);
437
+ const expiresAt = Math.floor(now.getTime() / 1e3) + ttlSeconds;
438
+ try {
439
+ await dynamodb.send(new client_dynamodb_1.PutItemCommand({
440
+ TableName: tableName,
441
+ Item: {
442
+ consumerName: { S: input.consumerName },
443
+ sk: { S: sk },
444
+ eventId: { S: input.eventId },
445
+ attempt: { N: String(input.attempt) },
446
+ recordedAt: { S: now.toISOString() },
447
+ expiresAt: { N: String(expiresAt) }
448
+ },
449
+ ConditionExpression: "attribute_not_exists(consumerName) AND attribute_not_exists(sk)"
450
+ }));
451
+ return { recorded: true };
452
+ } catch (err) {
453
+ if (err instanceof client_dynamodb_1.ConditionalCheckFailedException) {
454
+ return { recorded: false, alreadyProcessed: true };
455
+ }
456
+ throw err;
457
+ }
458
+ }
459
+ async function markFailed(dynamodb, input, options = {}) {
460
+ assertConsumerName(input.consumerName);
461
+ assertPositiveInteger(input.attempt, "attempt");
462
+ if (input.reason.length === 0) {
463
+ throw new WorkflowDedupInvalidInputError("reason must be non-empty.");
464
+ }
465
+ const tableName = resolveTableName(options.tableName);
466
+ const now = (options.now ?? defaultNow)();
467
+ const sk = encodeSortKey(input.eventId, input.attempt);
468
+ await dynamodb.send(new client_dynamodb_1.UpdateItemCommand({
469
+ TableName: tableName,
470
+ Key: {
471
+ consumerName: { S: input.consumerName },
472
+ sk: { S: sk }
473
+ },
474
+ UpdateExpression: "SET #failed = :failed, #failureReason = :reason, #failedAt = :failedAt",
475
+ ExpressionAttributeNames: {
476
+ "#failed": "failed",
477
+ "#failureReason": "failureReason",
478
+ "#failedAt": "failedAt"
479
+ },
480
+ ExpressionAttributeValues: {
481
+ ":failed": { BOOL: true },
482
+ ":reason": { S: input.reason },
483
+ ":failedAt": { S: now.toISOString() }
484
+ }
485
+ }));
486
+ }
487
+ function encodeSortKey(eventId, attempt) {
488
+ if (eventId.length === 0) {
489
+ throw new WorkflowDedupInvalidInputError("eventId must be non-empty.");
490
+ }
491
+ return `${eventId}#${attempt}`;
492
+ }
493
+ function resolveTableName(explicit) {
494
+ const name = explicit ?? process.env[env_1.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR];
495
+ if (!name) {
496
+ throw new WorkflowDedupTableNameMissingError(`Workflow dedup table name not set. Pass options.tableName or set ${env_1.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR}.`);
497
+ }
498
+ return name;
499
+ }
500
+ function assertConsumerName(consumerName) {
501
+ if (consumerName.length === 0) {
502
+ throw new WorkflowDedupInvalidInputError("consumerName must be non-empty.");
503
+ }
504
+ if (consumerName.length > env_1.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH) {
505
+ throw new WorkflowDedupInvalidInputError(`consumerName must be \u2264${env_1.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH} chars; got ${consumerName.length}.`);
506
+ }
507
+ if (/\s/.test(consumerName)) {
508
+ throw new WorkflowDedupInvalidInputError("consumerName must not contain whitespace.");
509
+ }
510
+ }
511
+ function assertPositiveInteger(value, field) {
512
+ if (!Number.isInteger(value) || value < 1) {
513
+ throw new WorkflowDedupInvalidInputError(`${field} must be a 1-indexed integer; got ${value}.`);
514
+ }
515
+ }
516
+ function defaultNow() {
517
+ return /* @__PURE__ */ new Date();
518
+ }
519
+ var WorkflowDedupTableNameMissingError = class extends Error {
520
+ /** @param message - human-readable description. */
521
+ constructor(message) {
522
+ super(message);
523
+ this.name = "WorkflowDedupTableNameMissingError";
524
+ }
525
+ };
526
+ exports2.WorkflowDedupTableNameMissingError = WorkflowDedupTableNameMissingError;
527
+ var WorkflowDedupInvalidInputError = class extends Error {
528
+ /** @param message - human-readable description. */
529
+ constructor(message) {
530
+ super(message);
531
+ this.name = "WorkflowDedupInvalidInputError";
532
+ }
533
+ };
534
+ exports2.WorkflowDedupInvalidInputError = WorkflowDedupInvalidInputError;
535
+ }
536
+ });
537
+
538
+ // ../workflows/lib/dedup/index.js
539
+ var require_dedup = __commonJS({
540
+ "../workflows/lib/dedup/index.js"(exports2) {
541
+ "use strict";
542
+ Object.defineProperty(exports2, "__esModule", { value: true });
543
+ exports2.workflowDedupClient = exports2.recordIfAbsent = exports2.markFailed = exports2.encodeSortKey = exports2.WorkflowDedupTableNameMissingError = exports2.WorkflowDedupInvalidInputError = exports2.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR = exports2.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH = exports2.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS = void 0;
544
+ var env_1 = require_env();
545
+ Object.defineProperty(exports2, "WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS", { enumerable: true, get: function() {
546
+ return env_1.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS;
547
+ } });
548
+ Object.defineProperty(exports2, "WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH", { enumerable: true, get: function() {
549
+ return env_1.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH;
550
+ } });
551
+ Object.defineProperty(exports2, "WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR", { enumerable: true, get: function() {
552
+ return env_1.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR;
553
+ } });
554
+ var workflow_dedup_client_1 = require_workflow_dedup_client();
555
+ Object.defineProperty(exports2, "WorkflowDedupInvalidInputError", { enumerable: true, get: function() {
556
+ return workflow_dedup_client_1.WorkflowDedupInvalidInputError;
557
+ } });
558
+ Object.defineProperty(exports2, "WorkflowDedupTableNameMissingError", { enumerable: true, get: function() {
559
+ return workflow_dedup_client_1.WorkflowDedupTableNameMissingError;
560
+ } });
561
+ Object.defineProperty(exports2, "encodeSortKey", { enumerable: true, get: function() {
562
+ return workflow_dedup_client_1.encodeSortKey;
563
+ } });
564
+ Object.defineProperty(exports2, "markFailed", { enumerable: true, get: function() {
565
+ return workflow_dedup_client_1.markFailed;
566
+ } });
567
+ Object.defineProperty(exports2, "recordIfAbsent", { enumerable: true, get: function() {
568
+ return workflow_dedup_client_1.recordIfAbsent;
569
+ } });
570
+ Object.defineProperty(exports2, "workflowDedupClient", { enumerable: true, get: function() {
571
+ return workflow_dedup_client_1.workflowDedupClient;
572
+ } });
573
+ }
574
+ });
575
+
576
+ // ../workflows/lib/index.js
577
+ var require_lib = __commonJS({
578
+ "../workflows/lib/index.js"(exports2) {
579
+ "use strict";
580
+ Object.defineProperty(exports2, "__esModule", { value: true });
581
+ exports2.workflowDedupClient = exports2.recordIfAbsent = exports2.markFailed = exports2.encodeSortKey = exports2.WorkflowDedupTableNameMissingError = exports2.WorkflowDedupInvalidInputError = exports2.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR = exports2.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH = exports2.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS = exports2.parseWorkflowEvent = exports2.UnsupportedEnvelopeVersionError = exports2.InvalidWorkflowEventError = exports2.workflowsClient = exports2.publishWorkflowEvent = exports2.WorkflowPublishError = exports2.isWellFormedDetailType = exports2.defineDetailType = exports2.RENAMABLE_ENTITY_TYPE = exports2.PlatformSystemDataSeededV1 = exports2.PlatformDeploymentCompletedV1 = exports2.OWNING_ENTITY_TYPE = exports2.InvalidDetailTypeRegistrationError = exports2.ControlPlaneRenameV1 = exports2.ControlPlaneRenameFailedV1 = exports2.ControlPlaneRenameCompleteV1 = exports2.ControlPlaneOwningDeleteV1 = exports2.ControlPlaneOwningDeleteFailedV1 = exports2.ControlPlaneOwningDeleteCompleteV1 = exports2.OPENHI_OPS_SOURCE = exports2.OPENHI_DATA_SOURCE = exports2.OPENHI_CONTROL_SOURCE = exports2.DEFAULT_BUS_NAME_BY_SOURCE = exports2.workflowUserActorFromClaims = exports2.isWorkflowUserActor = exports2.isWorkflowSystemActor = exports2.MissingActorContextError = exports2.isSupportedEnvelopeVersion = exports2.ENVELOPE_VERSION = void 0;
582
+ var envelope_version_1 = require_envelope_version();
583
+ Object.defineProperty(exports2, "ENVELOPE_VERSION", { enumerable: true, get: function() {
584
+ return envelope_version_1.ENVELOPE_VERSION;
585
+ } });
586
+ Object.defineProperty(exports2, "isSupportedEnvelopeVersion", { enumerable: true, get: function() {
587
+ return envelope_version_1.isSupportedEnvelopeVersion;
588
+ } });
589
+ var envelope_1 = require_envelope();
590
+ Object.defineProperty(exports2, "MissingActorContextError", { enumerable: true, get: function() {
591
+ return envelope_1.MissingActorContextError;
592
+ } });
593
+ Object.defineProperty(exports2, "isWorkflowSystemActor", { enumerable: true, get: function() {
594
+ return envelope_1.isWorkflowSystemActor;
595
+ } });
596
+ Object.defineProperty(exports2, "isWorkflowUserActor", { enumerable: true, get: function() {
597
+ return envelope_1.isWorkflowUserActor;
598
+ } });
599
+ Object.defineProperty(exports2, "workflowUserActorFromClaims", { enumerable: true, get: function() {
600
+ return envelope_1.workflowUserActorFromClaims;
601
+ } });
602
+ var sources_1 = require_sources();
603
+ Object.defineProperty(exports2, "DEFAULT_BUS_NAME_BY_SOURCE", { enumerable: true, get: function() {
604
+ return sources_1.DEFAULT_BUS_NAME_BY_SOURCE;
605
+ } });
606
+ Object.defineProperty(exports2, "OPENHI_CONTROL_SOURCE", { enumerable: true, get: function() {
607
+ return sources_1.OPENHI_CONTROL_SOURCE;
608
+ } });
609
+ Object.defineProperty(exports2, "OPENHI_DATA_SOURCE", { enumerable: true, get: function() {
610
+ return sources_1.OPENHI_DATA_SOURCE;
611
+ } });
612
+ Object.defineProperty(exports2, "OPENHI_OPS_SOURCE", { enumerable: true, get: function() {
613
+ return sources_1.OPENHI_OPS_SOURCE;
614
+ } });
615
+ var detail_types_1 = require_detail_types();
616
+ Object.defineProperty(exports2, "ControlPlaneOwningDeleteCompleteV1", { enumerable: true, get: function() {
617
+ return detail_types_1.ControlPlaneOwningDeleteCompleteV1;
618
+ } });
619
+ Object.defineProperty(exports2, "ControlPlaneOwningDeleteFailedV1", { enumerable: true, get: function() {
620
+ return detail_types_1.ControlPlaneOwningDeleteFailedV1;
621
+ } });
622
+ Object.defineProperty(exports2, "ControlPlaneOwningDeleteV1", { enumerable: true, get: function() {
623
+ return detail_types_1.ControlPlaneOwningDeleteV1;
624
+ } });
625
+ Object.defineProperty(exports2, "ControlPlaneRenameCompleteV1", { enumerable: true, get: function() {
626
+ return detail_types_1.ControlPlaneRenameCompleteV1;
627
+ } });
628
+ Object.defineProperty(exports2, "ControlPlaneRenameFailedV1", { enumerable: true, get: function() {
629
+ return detail_types_1.ControlPlaneRenameFailedV1;
630
+ } });
631
+ Object.defineProperty(exports2, "ControlPlaneRenameV1", { enumerable: true, get: function() {
632
+ return detail_types_1.ControlPlaneRenameV1;
633
+ } });
634
+ Object.defineProperty(exports2, "InvalidDetailTypeRegistrationError", { enumerable: true, get: function() {
635
+ return detail_types_1.InvalidDetailTypeRegistrationError;
636
+ } });
637
+ Object.defineProperty(exports2, "OWNING_ENTITY_TYPE", { enumerable: true, get: function() {
638
+ return detail_types_1.OWNING_ENTITY_TYPE;
639
+ } });
640
+ Object.defineProperty(exports2, "PlatformDeploymentCompletedV1", { enumerable: true, get: function() {
641
+ return detail_types_1.PlatformDeploymentCompletedV1;
642
+ } });
643
+ Object.defineProperty(exports2, "PlatformSystemDataSeededV1", { enumerable: true, get: function() {
644
+ return detail_types_1.PlatformSystemDataSeededV1;
645
+ } });
646
+ Object.defineProperty(exports2, "RENAMABLE_ENTITY_TYPE", { enumerable: true, get: function() {
647
+ return detail_types_1.RENAMABLE_ENTITY_TYPE;
648
+ } });
649
+ Object.defineProperty(exports2, "defineDetailType", { enumerable: true, get: function() {
650
+ return detail_types_1.defineDetailType;
651
+ } });
652
+ Object.defineProperty(exports2, "isWellFormedDetailType", { enumerable: true, get: function() {
653
+ return detail_types_1.isWellFormedDetailType;
654
+ } });
655
+ var publisher_1 = require_publisher();
656
+ Object.defineProperty(exports2, "WorkflowPublishError", { enumerable: true, get: function() {
657
+ return publisher_1.WorkflowPublishError;
658
+ } });
659
+ Object.defineProperty(exports2, "publishWorkflowEvent", { enumerable: true, get: function() {
660
+ return publisher_1.publishWorkflowEvent;
661
+ } });
662
+ Object.defineProperty(exports2, "workflowsClient", { enumerable: true, get: function() {
663
+ return publisher_1.workflowsClient;
664
+ } });
665
+ var consumer_1 = require_consumer();
666
+ Object.defineProperty(exports2, "InvalidWorkflowEventError", { enumerable: true, get: function() {
667
+ return consumer_1.InvalidWorkflowEventError;
668
+ } });
669
+ Object.defineProperty(exports2, "UnsupportedEnvelopeVersionError", { enumerable: true, get: function() {
670
+ return consumer_1.UnsupportedEnvelopeVersionError;
671
+ } });
672
+ Object.defineProperty(exports2, "parseWorkflowEvent", { enumerable: true, get: function() {
673
+ return consumer_1.parseWorkflowEvent;
674
+ } });
675
+ var dedup_1 = require_dedup();
676
+ Object.defineProperty(exports2, "WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS", { enumerable: true, get: function() {
677
+ return dedup_1.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS;
678
+ } });
679
+ Object.defineProperty(exports2, "WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH", { enumerable: true, get: function() {
680
+ return dedup_1.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH;
681
+ } });
682
+ Object.defineProperty(exports2, "WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR", { enumerable: true, get: function() {
683
+ return dedup_1.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR;
684
+ } });
685
+ Object.defineProperty(exports2, "WorkflowDedupInvalidInputError", { enumerable: true, get: function() {
686
+ return dedup_1.WorkflowDedupInvalidInputError;
687
+ } });
688
+ Object.defineProperty(exports2, "WorkflowDedupTableNameMissingError", { enumerable: true, get: function() {
689
+ return dedup_1.WorkflowDedupTableNameMissingError;
690
+ } });
691
+ Object.defineProperty(exports2, "encodeSortKey", { enumerable: true, get: function() {
692
+ return dedup_1.encodeSortKey;
693
+ } });
694
+ Object.defineProperty(exports2, "markFailed", { enumerable: true, get: function() {
695
+ return dedup_1.markFailed;
696
+ } });
697
+ Object.defineProperty(exports2, "recordIfAbsent", { enumerable: true, get: function() {
698
+ return dedup_1.recordIfAbsent;
699
+ } });
700
+ Object.defineProperty(exports2, "workflowDedupClient", { enumerable: true, get: function() {
701
+ return dedup_1.workflowDedupClient;
702
+ } });
703
+ }
704
+ });
705
+
706
+ // src/workflows/control-plane/owning-delete-cascade/delete-chunk.handler.ts
707
+ var delete_chunk_handler_exports = {};
708
+ __export(delete_chunk_handler_exports, {
709
+ handler: () => handler
710
+ });
711
+ module.exports = __toCommonJS(delete_chunk_handler_exports);
712
+
713
+ // src/data/dynamo/dynamo-control-service.ts
714
+ var import_electrodb14 = require("electrodb");
715
+
716
+ // src/data/dynamo/dynamo-client.ts
717
+ var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
718
+ var defaultTableName = process.env.DYNAMO_TABLE_NAME ?? "jesttesttable";
719
+ var dynamoClient = new import_client_dynamodb.DynamoDBClient({
720
+ ...process.env.MOCK_DYNAMODB_ENDPOINT && {
721
+ endpoint: process.env.MOCK_DYNAMODB_ENDPOINT,
722
+ sslEnabled: false,
723
+ region: "local"
724
+ }
725
+ });
726
+
727
+ // src/data/dynamo/entities/control/configuration-entity.ts
728
+ var import_electrodb = require("electrodb");
729
+
730
+ // src/data/dynamo/entities/control/control-entity-common.ts
731
+ var import_types = require("@openhi/types");
732
+
733
+ // src/data/dynamo/shard.ts
734
+ var SHARD_COUNT = 4;
735
+ function computeShard(id) {
736
+ let hash = 2166136261;
737
+ for (let i = 0; i < id.length; i++) {
738
+ hash ^= id.charCodeAt(i);
739
+ hash = Math.imul(hash, 16777619);
740
+ }
741
+ return (hash >>> 0) % SHARD_COUNT;
742
+ }
743
+
744
+ // src/data/dynamo/entities/control/control-entity-common.ts
745
+ var gsi1ShardAttribute = {
746
+ type: "string",
747
+ watch: ["id"],
748
+ set: (_val, item) => {
749
+ if (typeof item?.id !== "string" || item.id.length === 0) {
750
+ return void 0;
751
+ }
752
+ return String(computeShard(item.id));
753
+ }
754
+ };
755
+ var gsi1skAttribute = {
756
+ type: "string",
757
+ watch: ["resource", "lastUpdated", "id"],
758
+ set: (_val, item) => {
759
+ const id = typeof item?.id === "string" ? item.id : "";
760
+ const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
761
+ const fallback = `${lastUpdated}#${id}`;
762
+ if (typeof item?.resource !== "string" || item.resource.length === 0) {
763
+ return fallback;
764
+ }
765
+ let parsed;
766
+ try {
767
+ parsed = JSON.parse(item.resource);
768
+ } catch {
769
+ return fallback;
770
+ }
771
+ if (!parsed || typeof parsed !== "object") return fallback;
772
+ const resourceType = parsed.resourceType;
773
+ if (typeof resourceType !== "string") return fallback;
774
+ const label = (0, import_types.extractLabel)(parsed);
775
+ return label !== void 0 ? `${label}#${id}` : fallback;
776
+ }
777
+ };
778
+ function extractRoleId(resource) {
779
+ const flat = resource.roleId;
780
+ if (typeof flat === "string" && flat.length > 0) return flat;
781
+ const role = resource.role;
782
+ if (role && typeof role === "object") {
783
+ const reference = role.reference;
784
+ if (typeof reference === "string" && reference.length > 0) {
785
+ const slash = reference.lastIndexOf("/");
786
+ const tail = slash >= 0 ? reference.slice(slash + 1) : reference;
787
+ if (tail.length > 0) return tail;
788
+ }
789
+ }
790
+ return void 0;
791
+ }
792
+ var roleAssignmentGsi1skAttribute = {
793
+ type: "string",
794
+ watch: ["resource", "denormalizedUserName", "lastUpdated", "id"],
795
+ set: (_val, item) => {
796
+ const id = typeof item?.id === "string" ? item.id : "";
797
+ const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
798
+ const fallback = `${lastUpdated}#${id}`;
799
+ if (typeof item?.resource !== "string" || item.resource.length === 0) {
800
+ return fallback;
801
+ }
802
+ let parsed;
803
+ try {
804
+ parsed = JSON.parse(item.resource);
805
+ } catch {
806
+ return fallback;
807
+ }
808
+ if (!parsed || typeof parsed !== "object") return fallback;
809
+ const roleId = extractRoleId(parsed);
810
+ if (roleId === void 0) return fallback;
811
+ const denormalizedUserName = typeof item.denormalizedUserName === "string" ? item.denormalizedUserName : "";
812
+ const normalizedUserName = denormalizedUserName.length > 0 ? (0, import_types.normalizeLabel)(denormalizedUserName) : "";
813
+ if (normalizedUserName.length === 0) return fallback;
814
+ return `${roleId}#${normalizedUserName}#${id}`;
815
+ }
816
+ };
817
+ var membershipGsi1skAttribute = {
818
+ type: "string",
819
+ watch: ["denormalizedUserName", "lastUpdated", "id"],
820
+ set: (_val, item) => {
821
+ const id = typeof item?.id === "string" ? item.id : "";
822
+ const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
823
+ const fallback = `${lastUpdated}#${id}`;
824
+ const denormalizedUserName = typeof item?.denormalizedUserName === "string" ? item.denormalizedUserName : "";
825
+ const normalizedUserName = denormalizedUserName.length > 0 ? (0, import_types.normalizeLabel)(denormalizedUserName) : "";
826
+ if (normalizedUserName.length === 0) {
827
+ return fallback;
828
+ }
829
+ return `${normalizedUserName}#${id}`;
830
+ }
831
+ };
832
+
833
+ // src/data/dynamo/entities/control/configuration-entity.ts
834
+ var ConfigurationEntity = new import_electrodb.Entity({
835
+ model: {
836
+ entity: "configuration",
837
+ service: "control",
838
+ version: "01"
839
+ },
840
+ attributes: {
841
+ /** Sort key. "CURRENT" for current version; version history in S3. */
842
+ sk: {
843
+ type: "string",
844
+ required: true,
845
+ default: "CURRENT"
846
+ },
847
+ /** Tenant scope. Use "BASELINE" when the config is baseline default (no tenant). */
848
+ tenantId: {
849
+ type: "string",
850
+ required: true,
851
+ default: "BASELINE"
852
+ },
853
+ /** Workspace scope. Use "-" when absent. */
854
+ workspaceId: {
855
+ type: "string",
856
+ required: true,
857
+ default: "-"
858
+ },
859
+ /** User scope. Use "-" when absent. */
860
+ userId: {
861
+ type: "string",
862
+ required: true,
863
+ default: "-"
864
+ },
865
+ /** Role scope. Use "-" when absent. */
866
+ roleId: {
867
+ type: "string",
868
+ required: true,
869
+ default: "-"
870
+ },
871
+ /** Config type (category), e.g. endpoints, branding, display. */
872
+ key: {
873
+ type: "string",
874
+ required: true
875
+ },
876
+ /** FHIR Resource.id; logical id in URL and for the Configuration resource. */
877
+ id: {
878
+ type: "string",
879
+ required: true
880
+ },
881
+ /** Payload as JSON string. JSON.stringify(resource) on write; JSON.parse(item.resource) on read. */
882
+ resource: {
883
+ type: "string",
884
+ required: true
885
+ },
886
+ /**
887
+ * Summary projection (key display fields as JSON string: id, key, status).
888
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
889
+ */
890
+ summary: {
891
+ type: "string",
892
+ required: true
893
+ },
894
+ /** Version id (e.g. ULID). Tracks current version; S3 history key. */
895
+ vid: {
896
+ type: "string",
897
+ required: true
898
+ },
899
+ lastUpdated: {
900
+ type: "string",
901
+ required: true
902
+ },
903
+ gsi1Shard: gsi1ShardAttribute,
904
+ deleted: {
905
+ type: "boolean",
906
+ required: false
907
+ },
908
+ bundleId: {
909
+ type: "string",
910
+ required: false
911
+ },
912
+ msgId: {
913
+ type: "string",
914
+ required: false
915
+ }
916
+ },
917
+ indexes: {
918
+ /** 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. */
919
+ record: {
920
+ pk: {
921
+ field: "PK",
922
+ composite: ["tenantId", "workspaceId", "userId", "roleId"],
923
+ template: "CONFIG#TID#${tenantId}#WID#${workspaceId}#UID#${userId}#RID#${roleId}"
924
+ },
925
+ sk: {
926
+ field: "SK",
927
+ composite: ["key", "sk"],
928
+ template: "KEY#${key}#SK#${sk}"
929
+ }
930
+ },
931
+ /**
932
+ * GSI1 — Unified Sharded List per ADR-011: list all Configuration entries for a
933
+ * (tenant, workspace) across the four shards. Use for "list configs scoped to this tenant"
934
+ * (workspaceId = "-") or "list configs scoped to this workspace". Does not support
935
+ * hierarchical resolution in one query; use base table GetItem in fallback order
936
+ * (user → workspace → tenant → baseline) for that.
937
+ * SK is `<key>#<id>` — Configuration's `key` is a required entity attribute (the
938
+ * config category: endpoints, branding, display, …) and the natural sort/lookup
939
+ * dimension. `casing: "none"` preserves the literal key value.
940
+ */
941
+ gsi1: {
942
+ index: "GSI1",
943
+ pk: {
944
+ field: "GSI1PK",
945
+ composite: ["tenantId", "workspaceId", "gsi1Shard"],
946
+ template: "TID#${tenantId}#WID#${workspaceId}#RT#Configuration#SHARD#${gsi1Shard}"
947
+ },
948
+ sk: {
949
+ field: "GSI1SK",
950
+ casing: "none",
951
+ composite: ["key", "id"],
952
+ template: "${key}#${id}"
953
+ }
954
+ }
955
+ }
956
+ });
957
+
958
+ // src/data/dynamo/entities/control/configuration-user-projection-entity.ts
959
+ var import_electrodb2 = require("electrodb");
960
+ var ConfigurationUserProjectionEntity = new import_electrodb2.Entity({
961
+ model: {
962
+ entity: "configurationUserProjection",
963
+ service: "control",
964
+ version: "01"
965
+ },
966
+ attributes: {
967
+ /**
968
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
969
+ * base-table PK. Always required — the projection has no meaning
970
+ * outside a user partition.
971
+ */
972
+ userId: {
973
+ type: "string",
974
+ required: true
975
+ },
976
+ /**
977
+ * Pre-composed sort key — built by the operations-layer projection
978
+ * writer via `buildConfigurationUserProjectionSk`. The entity stores
979
+ * the value verbatim so the SK grammar (pattern #10 user-scope) is
980
+ * owned by the operations layer, not duplicated here.
981
+ */
982
+ sk: {
983
+ type: "string",
984
+ required: true
985
+ },
986
+ /**
987
+ * Configuration canonical-record id. Stored as a discriminating
988
+ * field so consumers can hydrate the canonical row via the
989
+ * Configuration get-by-id operation when the projection's `summary`
990
+ * is insufficient.
991
+ */
992
+ configurationId: {
993
+ type: "string",
994
+ required: true
995
+ },
996
+ /**
997
+ * Tenant the Configuration is associated with. The canonical row
998
+ * keys off `(tenantId, workspaceId, userId, roleId)`; the projection
999
+ * carries `tenantId` so consumers reconstructing the canonical PK
1000
+ * have the tenant segment without a hop.
1001
+ */
1002
+ tenantId: {
1003
+ type: "string",
1004
+ required: true
1005
+ },
1006
+ /**
1007
+ * Scope marker. Always `"user"` on this projection — recorded
1008
+ * explicitly so future scope-bearing projections (workspace,
1009
+ * tenant, role) can share filter semantics in a unified
1010
+ * cross-projection list query if one ever lands.
1011
+ */
1012
+ scope: {
1013
+ type: "string",
1014
+ required: true,
1015
+ default: "user"
1016
+ },
1017
+ /**
1018
+ * Configuration's `key` attribute (config category, e.g. endpoints,
1019
+ * branding, display). Mirrored from the canonical row so consumers
1020
+ * reading the projection get the natural display label without a
1021
+ * BatchGet hop. Doubles as the source of `<normalizedConfigName>` in
1022
+ * the SK.
1023
+ */
1024
+ displayName: {
1025
+ type: "string",
1026
+ required: false
1027
+ },
1028
+ /**
1029
+ * Summary projection (key display fields as JSON string) — mirrored
1030
+ * from the canonical Configuration row so user-partition queries do
1031
+ * not need a BatchGet hop.
1032
+ */
1033
+ summary: {
1034
+ type: "string",
1035
+ required: true
1036
+ },
1037
+ /** Version id mirrored from the canonical Configuration row. */
1038
+ vid: {
1039
+ type: "string",
1040
+ required: true
1041
+ },
1042
+ /** Last-updated timestamp mirrored from the canonical Configuration row. */
1043
+ lastUpdated: {
1044
+ type: "string",
1045
+ required: true
1046
+ }
1047
+ },
1048
+ indexes: {
1049
+ /**
1050
+ * Base table: PK = USER#ID#<userId>, SK = operation-supplied. A
1051
+ * single `Query(PK = USER#ID#<userId>, SK begins_with
1052
+ * 'CONFIGURATION#')` returns the user's user-scoped Configurations
1053
+ * sorted by `<normalizedConfigName>` (then `<configurationId>` as
1054
+ * the tiebreaker).
1055
+ */
1056
+ record: {
1057
+ pk: {
1058
+ field: "PK",
1059
+ composite: ["userId"],
1060
+ template: "USER#ID#${userId}"
1061
+ },
1062
+ sk: {
1063
+ field: "SK",
1064
+ casing: "none",
1065
+ composite: ["sk"],
1066
+ template: "${sk}"
1067
+ }
1068
+ }
1069
+ }
1070
+ });
1071
+
1072
+ // src/data/dynamo/entities/control/configuration-workspace-projection-entity.ts
1073
+ var import_electrodb3 = require("electrodb");
1074
+ var ConfigurationWorkspaceProjectionEntity = new import_electrodb3.Entity({
1075
+ model: {
1076
+ entity: "configurationWorkspaceProjection",
1077
+ service: "control",
1078
+ version: "01"
1079
+ },
1080
+ attributes: {
1081
+ /**
1082
+ * Tenant the workspace belongs to. Renders as the leading segment
1083
+ * of the base-table PK. Always required — the workspace partition
1084
+ * is tenant-scoped per ADR-011.
1085
+ */
1086
+ tenantId: {
1087
+ type: "string",
1088
+ required: true
1089
+ },
1090
+ /**
1091
+ * Workspace partition discriminator. Renders as the trailing
1092
+ * segment of the base-table PK
1093
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
1094
+ * the projection has no meaning outside a workspace partition.
1095
+ */
1096
+ workspaceId: {
1097
+ type: "string",
1098
+ required: true
1099
+ },
1100
+ /**
1101
+ * Pre-composed sort key — built by the operations-layer projection
1102
+ * writer via `buildConfigurationWorkspaceProjectionSk`. The entity
1103
+ * stores the value verbatim so the SK grammar (pattern #10
1104
+ * workspace-scope) is owned by the operations layer, not
1105
+ * duplicated here.
1106
+ */
1107
+ sk: {
1108
+ type: "string",
1109
+ required: true
1110
+ },
1111
+ /**
1112
+ * Configuration canonical-record id. Stored as a discriminating
1113
+ * field so consumers can hydrate the canonical row via the
1114
+ * Configuration get-by-id operation when the projection's `summary`
1115
+ * is insufficient.
1116
+ */
1117
+ configurationId: {
1118
+ type: "string",
1119
+ required: true
1120
+ },
1121
+ /**
1122
+ * Scope marker. Always `"workspace"` on this projection — recorded
1123
+ * explicitly so future scope-bearing projections (user, tenant,
1124
+ * role) can share filter semantics in a unified cross-projection
1125
+ * list query if one ever lands.
1126
+ */
1127
+ scope: {
1128
+ type: "string",
1129
+ required: true,
1130
+ default: "workspace"
1131
+ },
1132
+ /**
1133
+ * Configuration's `key` attribute (config category, e.g. endpoints,
1134
+ * branding, display). Mirrored from the canonical row so consumers
1135
+ * reading the projection get the natural display label without a
1136
+ * BatchGet hop. Doubles as the source of `<normalizedConfigName>`
1137
+ * in the SK.
1138
+ */
1139
+ displayName: {
1140
+ type: "string",
1141
+ required: false
1142
+ },
1143
+ /**
1144
+ * Summary projection (key display fields as JSON string) — mirrored
1145
+ * from the canonical Configuration row so workspace-partition
1146
+ * queries do not need a BatchGet hop.
1147
+ */
1148
+ summary: {
1149
+ type: "string",
1150
+ required: true
1151
+ },
1152
+ /** Version id mirrored from the canonical Configuration row. */
1153
+ vid: {
1154
+ type: "string",
1155
+ required: true
1156
+ },
1157
+ /** Last-updated timestamp mirrored from the canonical Configuration row. */
1158
+ lastUpdated: {
1159
+ type: "string",
1160
+ required: true
1161
+ }
1162
+ },
1163
+ indexes: {
1164
+ /**
1165
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
1166
+ * SK = operation-supplied. A single
1167
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'CONFIGURATION#')`
1168
+ * returns the workspace's workspace-scoped Configurations sorted by
1169
+ * `<normalizedConfigName>` (then `<configurationId>` as the
1170
+ * tiebreaker).
1171
+ */
1172
+ record: {
1173
+ pk: {
1174
+ field: "PK",
1175
+ composite: ["tenantId", "workspaceId"],
1176
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
1177
+ },
1178
+ sk: {
1179
+ field: "SK",
1180
+ casing: "none",
1181
+ composite: ["sk"],
1182
+ template: "${sk}"
1183
+ }
1184
+ }
1185
+ }
1186
+ });
1187
+
1188
+ // src/data/dynamo/entities/control/membership-entity.ts
1189
+ var import_electrodb4 = require("electrodb");
1190
+ var MembershipEntity = new import_electrodb4.Entity({
1191
+ model: {
1192
+ entity: "membership",
1193
+ service: "control",
1194
+ version: "01"
1195
+ },
1196
+ attributes: {
1197
+ /** Sort key sentinel. Always "CURRENT". */
1198
+ sk: {
1199
+ type: "string",
1200
+ required: true,
1201
+ default: "CURRENT"
1202
+ },
1203
+ /** Tenant in which the user has membership (required). */
1204
+ tenantId: {
1205
+ type: "string",
1206
+ required: true
1207
+ },
1208
+ /** FHIR Resource.id; membership id. */
1209
+ id: {
1210
+ type: "string",
1211
+ required: true
1212
+ },
1213
+ /** Full Membership resource serialized as JSON string. */
1214
+ resource: {
1215
+ type: "string",
1216
+ required: true
1217
+ },
1218
+ /**
1219
+ * Summary projection (key display fields as JSON string: id, displayName, status).
1220
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
1221
+ */
1222
+ summary: {
1223
+ type: "string",
1224
+ required: true
1225
+ },
1226
+ /** Version id (e.g. ULID). */
1227
+ vid: {
1228
+ type: "string",
1229
+ required: true
1230
+ },
1231
+ lastUpdated: {
1232
+ type: "string",
1233
+ required: true
1234
+ },
1235
+ gsi1Shard: gsi1ShardAttribute,
1236
+ /**
1237
+ * Derived GSI1 sort key — `<normalizedUserName>#<id>` per ADR-018
1238
+ * pattern #1 so a GSI1 query partitioned on the tenant range-scans
1239
+ * by user-name prefix and returns memberships sorted by user name.
1240
+ * Falls back to `<lastUpdated>#<id>` when `denormalizedUserName`
1241
+ * is missing.
1242
+ */
1243
+ gsi1sk: membershipGsi1skAttribute,
1244
+ deleted: {
1245
+ type: "boolean",
1246
+ required: false
1247
+ },
1248
+ bundleId: {
1249
+ type: "string",
1250
+ required: false
1251
+ },
1252
+ msgId: {
1253
+ type: "string",
1254
+ required: false
1255
+ },
1256
+ /**
1257
+ * Denormalized `linked-data-identity` Reference (e.g. `Practitioner/abc`).
1258
+ * Populated from the FHIR extension on the Membership resource at write
1259
+ * time so future GSIs can index data-plane identity lookups without
1260
+ * deserializing the full resource JSON. See ADR 2026-03-13-02 §6.
1261
+ */
1262
+ linkedDataIdentityRef: {
1263
+ type: "string",
1264
+ required: false
1265
+ },
1266
+ /**
1267
+ * Denormalized display name of the linked Tenant, captured at row
1268
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1269
+ * adjacency-list projection SKs (pattern #3 — `MEMBERSHIP#TENANT#<normalizedTenantName>#…`)
1270
+ * can be composed from a top-level field instead of digging into the
1271
+ * `resource` JSON. Optional on the schema so pre-TR-024 rows do not
1272
+ * break; the operations-layer multi-write helper (#1010) makes the
1273
+ * field load-bearing at write time per TR-024 rule 2 (write-time
1274
+ * source = canonical Tenant.displayName).
1275
+ * @see TR-024 — Denormalized display-name attributes
1276
+ */
1277
+ denormalizedTenantName: {
1278
+ type: "string",
1279
+ required: false
1280
+ },
1281
+ /**
1282
+ * Denormalized display name of the linked User, captured at row
1283
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1284
+ * adjacency-list canonical-record GSI1SK (pattern #1 —
1285
+ * `<normalizedUserName>#<id>`) and workspace-projection SK (pattern #2)
1286
+ * can be composed from a top-level field. Optional on the schema so
1287
+ * pre-TR-024 rows do not break; the operations-layer multi-write helper
1288
+ * (#1010) makes the field load-bearing at write time per TR-024 rule 2
1289
+ * (write-time source = canonical User.displayName).
1290
+ * @see TR-024 — Denormalized display-name attributes
1291
+ */
1292
+ denormalizedUserName: {
1293
+ type: "string",
1294
+ required: false
1295
+ }
1296
+ },
1297
+ indexes: {
1298
+ /** Base table: PK = TID#<tenantId>#MEMBERSHIP#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
1299
+ record: {
1300
+ pk: {
1301
+ field: "PK",
1302
+ composite: ["tenantId", "id"],
1303
+ template: "TID#${tenantId}#MEMBERSHIP#ID#${id}"
1304
+ },
1305
+ sk: {
1306
+ field: "SK",
1307
+ composite: ["sk"],
1308
+ template: "${sk}"
1309
+ }
1310
+ },
1311
+ /**
1312
+ * GSI1 — Unified Sharded List per ADR-011: list all Memberships for a tenant across the
1313
+ * four shards. Membership is tenant-scoped only, so `WID#-` is a sentinel.
1314
+ * SK is derived via `membershipGsi1skAttribute` — composes
1315
+ * `<normalizedUserName>#<id>` per ADR-018 pattern #1 (users in a
1316
+ * tenant, sorted by user name); falls back to `<lastUpdated>#<id>`
1317
+ * when `denormalizedUserName` is missing. `casing: "none"` preserves
1318
+ * the normalized label and ISO-8601 `T`/`Z`.
1319
+ */
1320
+ gsi1: {
1321
+ index: "GSI1",
1322
+ pk: {
1323
+ field: "GSI1PK",
1324
+ composite: ["tenantId", "gsi1Shard"],
1325
+ template: "TID#${tenantId}#WID#-#RT#Membership#SHARD#${gsi1Shard}"
1326
+ },
1327
+ sk: {
1328
+ field: "GSI1SK",
1329
+ casing: "none",
1330
+ composite: ["gsi1sk"],
1331
+ template: "${gsi1sk}"
1332
+ }
1333
+ }
1334
+ }
1335
+ });
1336
+
1337
+ // src/data/dynamo/entities/control/membership-user-projection-entity.ts
1338
+ var import_electrodb5 = require("electrodb");
1339
+ var MembershipUserProjectionEntity = new import_electrodb5.Entity({
1340
+ model: {
1341
+ entity: "membershipUserProjection",
1342
+ service: "control",
1343
+ version: "01"
1344
+ },
1345
+ attributes: {
1346
+ /**
1347
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
1348
+ * base-table PK. Always required — the projection has no meaning
1349
+ * outside a user partition.
1350
+ */
1351
+ userId: {
1352
+ type: "string",
1353
+ required: true
1354
+ },
1355
+ /**
1356
+ * Pre-composed sort key — built by the operations-layer projection
1357
+ * writer via `buildMembershipUserProjectionSk*` helpers. The entity
1358
+ * stores the value verbatim so the SK grammar (patterns #3 and #4)
1359
+ * is owned by the operations layer, not duplicated here.
1360
+ */
1361
+ sk: {
1362
+ type: "string",
1363
+ required: true
1364
+ },
1365
+ /** Tenant in which the membership applies. Always required. */
1366
+ tenantId: {
1367
+ type: "string",
1368
+ required: true
1369
+ },
1370
+ /**
1371
+ * Workspace the membership scopes to. Present iff the projection
1372
+ * row is a pattern-#4 workspace sub-lane row; absent for pattern-#3
1373
+ * tenant sub-lane rows.
1374
+ */
1375
+ workspaceId: {
1376
+ type: "string",
1377
+ required: false
1378
+ },
1379
+ /**
1380
+ * Membership canonical-record id. Stored as a discriminating field
1381
+ * so consumers can hydrate the canonical row via
1382
+ * `MembershipEntity.get({ tenantId, id: membershipId })` when the
1383
+ * projection's `summary` is insufficient.
1384
+ */
1385
+ membershipId: {
1386
+ type: "string",
1387
+ required: true
1388
+ },
1389
+ /**
1390
+ * Summary projection (key display fields as JSON string: id,
1391
+ * displayName, status) — mirrored from the canonical Membership row
1392
+ * so user-partition queries do not need a BatchGet hop.
1393
+ */
1394
+ summary: {
1395
+ type: "string",
1396
+ required: true
1397
+ },
1398
+ /** Version id mirrored from the canonical Membership row. */
1399
+ vid: {
1400
+ type: "string",
1401
+ required: true
1402
+ },
1403
+ /** Last-updated timestamp mirrored from the canonical Membership row. */
1404
+ lastUpdated: {
1405
+ type: "string",
1406
+ required: true
1407
+ },
1408
+ /**
1409
+ * Denormalized Tenant display name — required to compose pattern-#3
1410
+ * SK (`MEMBERSHIP#TENANT#<normalizedTenantName>#…`). Optional on the
1411
+ * schema because pre-TR-024 rows may not carry a display name; the
1412
+ * operations layer falls back gracefully when missing.
1413
+ */
1414
+ denormalizedTenantName: {
1415
+ type: "string",
1416
+ required: false
1417
+ },
1418
+ /**
1419
+ * Denormalized User display name — mirrored from the canonical
1420
+ * Membership row per TR-024 rule 3 (canonical-record symmetry).
1421
+ * Carried on the projection so consumers can render the user's
1422
+ * display name without a hop to the User record.
1423
+ */
1424
+ denormalizedUserName: {
1425
+ type: "string",
1426
+ required: false
1427
+ },
1428
+ /**
1429
+ * Denormalized Workspace display name — required to compose
1430
+ * pattern-#4 SK (`MEMBERSHIP#WORKSPACE#TID#<tenantId>#<normalizedWorkspaceName>#…`).
1431
+ * Optional on the schema (TR-024 § Open Item #4 defers a formal
1432
+ * Workspace-rename cascade); the operations layer falls back to a
1433
+ * sentinel when missing so the SK still has a valid shape.
1434
+ */
1435
+ denormalizedWorkspaceName: {
1436
+ type: "string",
1437
+ required: false
1438
+ }
1439
+ },
1440
+ indexes: {
1441
+ /**
1442
+ * Base table: PK = USER#ID#<userId>, SK = operation-supplied.
1443
+ * Both pattern #3 and pattern #4 use this same index — the SK string
1444
+ * encodes the lane discriminator (`MEMBERSHIP#TENANT#…` vs
1445
+ * `MEMBERSHIP#WORKSPACE#…`) so a single `Query(PK = USER#ID#<userId>,
1446
+ * SK begins_with 'MEMBERSHIP#')` returns both lanes interleaved.
1447
+ */
1448
+ record: {
1449
+ pk: {
1450
+ field: "PK",
1451
+ composite: ["userId"],
1452
+ template: "USER#ID#${userId}"
1453
+ },
1454
+ sk: {
1455
+ field: "SK",
1456
+ casing: "none",
1457
+ composite: ["sk"],
1458
+ template: "${sk}"
1459
+ }
1460
+ }
1461
+ }
1462
+ });
1463
+
1464
+ // src/data/dynamo/entities/control/membership-workspace-projection-entity.ts
1465
+ var import_electrodb6 = require("electrodb");
1466
+ var MembershipWorkspaceProjectionEntity = new import_electrodb6.Entity({
1467
+ model: {
1468
+ entity: "membershipWorkspaceProjection",
1469
+ service: "control",
1470
+ version: "01"
1471
+ },
1472
+ attributes: {
1473
+ /**
1474
+ * Tenant the workspace belongs to. Renders as the leading segment
1475
+ * of the base-table PK. Always required — the workspace partition
1476
+ * is tenant-scoped per ADR-011.
1477
+ */
1478
+ tenantId: {
1479
+ type: "string",
1480
+ required: true
1481
+ },
1482
+ /**
1483
+ * Workspace partition discriminator. Renders as the trailing
1484
+ * segment of the base-table PK
1485
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
1486
+ * the projection has no meaning outside a workspace partition.
1487
+ */
1488
+ workspaceId: {
1489
+ type: "string",
1490
+ required: true
1491
+ },
1492
+ /**
1493
+ * Pre-composed sort key — built by the operations-layer projection
1494
+ * writer via `buildMembershipWorkspaceProjectionSk`. The entity
1495
+ * stores the value verbatim so the SK grammar (pattern #2) is
1496
+ * owned by the operations layer, not duplicated here.
1497
+ */
1498
+ sk: {
1499
+ type: "string",
1500
+ required: true
1501
+ },
1502
+ /**
1503
+ * User the membership links. Stored as a discriminating field so
1504
+ * consumers can hydrate the canonical User row via
1505
+ * `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
1506
+ * projection's `summary` is insufficient.
1507
+ */
1508
+ userId: {
1509
+ type: "string",
1510
+ required: true
1511
+ },
1512
+ /**
1513
+ * Membership canonical-record id. Stored as a discriminating field
1514
+ * so consumers can hydrate the canonical row via
1515
+ * `MembershipEntity.get({ tenantId, id: membershipId })` when the
1516
+ * projection's `summary` is insufficient.
1517
+ */
1518
+ membershipId: {
1519
+ type: "string",
1520
+ required: true
1521
+ },
1522
+ /**
1523
+ * Summary projection (key display fields as JSON string: id,
1524
+ * displayName, status) — mirrored from the canonical Membership row
1525
+ * so workspace-partition queries do not need a BatchGet hop.
1526
+ */
1527
+ summary: {
1528
+ type: "string",
1529
+ required: true
1530
+ },
1531
+ /** Version id mirrored from the canonical Membership row. */
1532
+ vid: {
1533
+ type: "string",
1534
+ required: true
1535
+ },
1536
+ /** Last-updated timestamp mirrored from the canonical Membership row. */
1537
+ lastUpdated: {
1538
+ type: "string",
1539
+ required: true
1540
+ },
1541
+ /**
1542
+ * Denormalized User display name — required to compose the
1543
+ * pattern-#2 SK (`MEMBERSHIP#<normalizedUserName>#…`). Optional on
1544
+ * the schema because pre-TR-024 rows may not carry a display name;
1545
+ * the operations layer falls back to a sentinel when missing so
1546
+ * the SK still has a valid shape. The TR-023 rename-cascade
1547
+ * pipeline rewrites the SK on a User rename.
1548
+ */
1549
+ denormalizedUserName: {
1550
+ type: "string",
1551
+ required: false
1552
+ }
1553
+ },
1554
+ indexes: {
1555
+ /**
1556
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
1557
+ * SK = operation-supplied. Pattern #2 uses this index — the SK
1558
+ * encodes the entity-type prefix (`MEMBERSHIP#…`) so a
1559
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'MEMBERSHIP#')`
1560
+ * returns every member projection for the workspace in normalized-
1561
+ * user-name sort order.
1562
+ */
1563
+ record: {
1564
+ pk: {
1565
+ field: "PK",
1566
+ composite: ["tenantId", "workspaceId"],
1567
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
1568
+ },
1569
+ sk: {
1570
+ field: "SK",
1571
+ casing: "none",
1572
+ composite: ["sk"],
1573
+ template: "${sk}"
1574
+ }
1575
+ }
1576
+ }
1577
+ });
1578
+
1579
+ // src/data/dynamo/entities/control/role-entity.ts
1580
+ var import_electrodb7 = require("electrodb");
1581
+ var RoleEntity = new import_electrodb7.Entity({
1582
+ model: {
1583
+ entity: "role",
1584
+ service: "control",
1585
+ version: "01"
1586
+ },
1587
+ attributes: {
1588
+ /** Sort key sentinel. Always "CURRENT". */
1589
+ sk: {
1590
+ type: "string",
1591
+ required: true,
1592
+ default: "CURRENT"
1593
+ },
1594
+ /** FHIR Resource.id; role id. */
1595
+ id: {
1596
+ type: "string",
1597
+ required: true
1598
+ },
1599
+ /** Full Role resource serialized as JSON string. */
1600
+ resource: {
1601
+ type: "string",
1602
+ required: true
1603
+ },
1604
+ /**
1605
+ * Summary projection (key display fields as JSON string: id, displayName, status).
1606
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
1607
+ */
1608
+ summary: {
1609
+ type: "string",
1610
+ required: true
1611
+ },
1612
+ /** Version id (e.g. ULID). */
1613
+ vid: {
1614
+ type: "string",
1615
+ required: true
1616
+ },
1617
+ lastUpdated: {
1618
+ type: "string",
1619
+ required: true
1620
+ },
1621
+ gsi1Shard: gsi1ShardAttribute,
1622
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
1623
+ gsi1sk: gsi1skAttribute,
1624
+ deleted: {
1625
+ type: "boolean",
1626
+ required: false
1627
+ },
1628
+ bundleId: {
1629
+ type: "string",
1630
+ required: false
1631
+ },
1632
+ msgId: {
1633
+ type: "string",
1634
+ required: false
1635
+ }
1636
+ },
1637
+ indexes: {
1638
+ /** Base table: PK = ROLE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
1639
+ record: {
1640
+ pk: {
1641
+ field: "PK",
1642
+ composite: ["id"],
1643
+ template: "ROLE#ID#${id}"
1644
+ },
1645
+ sk: {
1646
+ field: "SK",
1647
+ composite: ["sk"],
1648
+ template: "${sk}"
1649
+ }
1650
+ },
1651
+ /**
1652
+ * GSI1 — Unified Sharded List per ADR-011: list all Roles across the four shards.
1653
+ * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#Role#SHARD#<n>`.
1654
+ * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
1655
+ * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
1656
+ * normalized label and ISO-8601 `T`/`Z`.
1657
+ */
1658
+ gsi1: {
1659
+ index: "GSI1",
1660
+ pk: {
1661
+ field: "GSI1PK",
1662
+ composite: ["gsi1Shard"],
1663
+ template: "TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}"
1664
+ },
1665
+ sk: {
1666
+ field: "GSI1SK",
1667
+ casing: "none",
1668
+ composite: ["gsi1sk"],
1669
+ template: "${gsi1sk}"
1670
+ }
1671
+ }
1672
+ }
1673
+ });
1674
+
1675
+ // src/data/dynamo/entities/control/roleassignment-entity.ts
1676
+ var import_electrodb8 = require("electrodb");
1677
+ var RoleAssignmentEntity = new import_electrodb8.Entity({
1678
+ model: {
1679
+ entity: "roleassignment",
1680
+ service: "control",
1681
+ version: "01"
1682
+ },
1683
+ attributes: {
1684
+ /** Sort key sentinel. Always "CURRENT". */
1685
+ sk: {
1686
+ type: "string",
1687
+ required: true,
1688
+ default: "CURRENT"
1689
+ },
1690
+ /** Tenant in which the role assignment applies (required). */
1691
+ tenantId: {
1692
+ type: "string",
1693
+ required: true
1694
+ },
1695
+ /** FHIR Resource.id; role assignment id. */
1696
+ id: {
1697
+ type: "string",
1698
+ required: true
1699
+ },
1700
+ /** Full RoleAssignment resource serialized as JSON string. */
1701
+ resource: {
1702
+ type: "string",
1703
+ required: true
1704
+ },
1705
+ /**
1706
+ * Summary projection (key display fields as JSON string: id, displayName, status).
1707
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
1708
+ */
1709
+ summary: {
1710
+ type: "string",
1711
+ required: true
1712
+ },
1713
+ /** Version id (e.g. ULID). */
1714
+ vid: {
1715
+ type: "string",
1716
+ required: true
1717
+ },
1718
+ lastUpdated: {
1719
+ type: "string",
1720
+ required: true
1721
+ },
1722
+ gsi1Shard: gsi1ShardAttribute,
1723
+ /**
1724
+ * Derived GSI1 sort key — discriminator-first
1725
+ * `<roleId>#<normalizedUserName>#<id>` per ADR-018 pattern #8 so a
1726
+ * GSI1 query partitioned on the tenant can `begins_with('<roleId>#')`
1727
+ * to enumerate every user assigned to a given role, sorted by user
1728
+ * name. Falls back to `<lastUpdated>#<id>` when either component is
1729
+ * missing.
1730
+ */
1731
+ gsi1sk: roleAssignmentGsi1skAttribute,
1732
+ deleted: {
1733
+ type: "boolean",
1734
+ required: false
1735
+ },
1736
+ bundleId: {
1737
+ type: "string",
1738
+ required: false
1739
+ },
1740
+ msgId: {
1741
+ type: "string",
1742
+ required: false
1743
+ },
1744
+ /**
1745
+ * Denormalized display name of the linked Tenant, captured at row
1746
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1747
+ * adjacency-list user-projection SK (pattern #5 —
1748
+ * `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#<roleId>#TID#<tenantId>#<id>`)
1749
+ * can be composed from a top-level field instead of digging into the
1750
+ * `resource` JSON. Optional on the schema so pre-TR-024 rows do not
1751
+ * break; the operations-layer multi-write helper (#1010) makes the
1752
+ * field load-bearing at write time per TR-024 rule 2 (write-time
1753
+ * source = canonical Tenant.displayName).
1754
+ * @see TR-024 — Denormalized display-name attributes
1755
+ */
1756
+ denormalizedTenantName: {
1757
+ type: "string",
1758
+ required: false
1759
+ },
1760
+ /**
1761
+ * Denormalized display name of the linked User, captured at row
1762
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1763
+ * adjacency-list canonical-record GSI1SK (pattern #8 —
1764
+ * `<roleId>#<normalizedUserName>#<id>`) and workspace-projection SK
1765
+ * (pattern #9) can be composed from a top-level field. Optional on
1766
+ * the schema so pre-TR-024 rows do not break; the operations-layer
1767
+ * multi-write helper (#1010) makes the field load-bearing at write
1768
+ * time per TR-024 rule 2 (write-time source = canonical
1769
+ * User.displayName).
1770
+ * @see TR-024 — Denormalized display-name attributes
1771
+ */
1772
+ denormalizedUserName: {
1773
+ type: "string",
1774
+ required: false
1775
+ },
1776
+ /**
1777
+ * Denormalized display name of the linked Role, captured at row
1778
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1779
+ * adjacency-list user-projection SK (pattern #5 —
1780
+ * `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#…`) can be composed from
1781
+ * a top-level field. Optional on the schema so pre-TR-024 rows do not
1782
+ * break; the operations-layer multi-write helper (#1010) makes the
1783
+ * field load-bearing at write time per TR-024 rule 2 (write-time
1784
+ * source = canonical Role.displayName).
1785
+ * @see TR-024 — Denormalized display-name attributes
1786
+ */
1787
+ denormalizedRoleName: {
1788
+ type: "string",
1789
+ required: false
1790
+ }
1791
+ },
1792
+ indexes: {
1793
+ /** Base table: PK = TID#<tenantId>#ROLEASSIGNMENT#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
1794
+ record: {
1795
+ pk: {
1796
+ field: "PK",
1797
+ composite: ["tenantId", "id"],
1798
+ template: "TID#${tenantId}#ROLEASSIGNMENT#ID#${id}"
1799
+ },
1800
+ sk: {
1801
+ field: "SK",
1802
+ composite: ["sk"],
1803
+ template: "${sk}"
1804
+ }
1805
+ },
1806
+ /**
1807
+ * GSI1 — Unified Sharded List per ADR-011: list all RoleAssignments for a tenant across the
1808
+ * four shards. Tenant-scoped only, so `WID#-` is a sentinel.
1809
+ * SK is derived via `roleAssignmentGsi1skAttribute` — composes the
1810
+ * discriminator-first `<roleId>#<normalizedUserName>#<id>` shape per
1811
+ * ADR-018 pattern #8 (users with a specific role in a tenant, sorted
1812
+ * by user name); falls back to `<lastUpdated>#<id>` when either
1813
+ * component is missing. `casing: "none"` preserves the normalized
1814
+ * label and ISO-8601 `T`/`Z`.
1815
+ */
1816
+ gsi1: {
1817
+ index: "GSI1",
1818
+ pk: {
1819
+ field: "GSI1PK",
1820
+ composite: ["tenantId", "gsi1Shard"],
1821
+ template: "TID#${tenantId}#WID#-#RT#RoleAssignment#SHARD#${gsi1Shard}"
1822
+ },
1823
+ sk: {
1824
+ field: "GSI1SK",
1825
+ casing: "none",
1826
+ composite: ["gsi1sk"],
1827
+ template: "${gsi1sk}"
1828
+ }
1829
+ }
1830
+ }
1831
+ });
1832
+
1833
+ // src/data/dynamo/entities/control/roleassignment-user-projection-entity.ts
1834
+ var import_electrodb9 = require("electrodb");
1835
+ var RoleAssignmentUserProjectionEntity = new import_electrodb9.Entity({
1836
+ model: {
1837
+ entity: "roleAssignmentUserProjection",
1838
+ service: "control",
1839
+ version: "01"
1840
+ },
1841
+ attributes: {
1842
+ /**
1843
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
1844
+ * base-table PK. Always required — the projection has no meaning
1845
+ * outside a user partition.
1846
+ */
1847
+ userId: {
1848
+ type: "string",
1849
+ required: true
1850
+ },
1851
+ /**
1852
+ * Pre-composed sort key — built by the operations-layer projection
1853
+ * writer via `buildRoleAssignmentUserProjectionSk*` helpers. The
1854
+ * entity stores the value verbatim so the SK grammar (tenant-lane
1855
+ * vs workspace-lane) is owned by the operations layer, not
1856
+ * duplicated here.
1857
+ */
1858
+ sk: {
1859
+ type: "string",
1860
+ required: true
1861
+ },
1862
+ /** Tenant in which the role assignment applies. Always required. */
1863
+ tenantId: {
1864
+ type: "string",
1865
+ required: true
1866
+ },
1867
+ /**
1868
+ * Workspace the role assignment scopes to. Present iff the
1869
+ * projection row is the workspace-level sub-lane; absent for
1870
+ * tenant-level sub-lane rows.
1871
+ */
1872
+ workspaceId: {
1873
+ type: "string",
1874
+ required: false
1875
+ },
1876
+ /**
1877
+ * Role the assignment grants. Stored as a discriminating field so
1878
+ * `Query(PK = USER#ID#<userId>, SK begins_with 'ROLEASSIGNMENT#…')`
1879
+ * results carry the role id without a hop to the canonical row.
1880
+ */
1881
+ roleId: {
1882
+ type: "string",
1883
+ required: true
1884
+ },
1885
+ /**
1886
+ * RoleAssignment canonical-record id. Stored as a discriminating
1887
+ * field so consumers can hydrate the canonical row via
1888
+ * `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
1889
+ * when the projection's `summary` is insufficient.
1890
+ */
1891
+ roleAssignmentId: {
1892
+ type: "string",
1893
+ required: true
1894
+ },
1895
+ /**
1896
+ * Summary projection (key display fields as JSON string: id,
1897
+ * displayName, status) — mirrored from the canonical RoleAssignment
1898
+ * row so user-partition queries do not need a BatchGet hop.
1899
+ */
1900
+ summary: {
1901
+ type: "string",
1902
+ required: true
1903
+ },
1904
+ /** Version id mirrored from the canonical RoleAssignment row. */
1905
+ vid: {
1906
+ type: "string",
1907
+ required: true
1908
+ },
1909
+ /** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
1910
+ lastUpdated: {
1911
+ type: "string",
1912
+ required: true
1913
+ },
1914
+ /**
1915
+ * Denormalized Tenant display name — mirrored from the canonical
1916
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
1917
+ * Optional on the schema because pre-TR-024 rows may not carry a
1918
+ * display name; the operations layer falls back gracefully when
1919
+ * missing.
1920
+ */
1921
+ denormalizedTenantName: {
1922
+ type: "string",
1923
+ required: false
1924
+ },
1925
+ /**
1926
+ * Denormalized User display name — mirrored from the canonical
1927
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
1928
+ * Carried on the projection so consumers can render the user's
1929
+ * display name without a hop to the User record.
1930
+ */
1931
+ denormalizedUserName: {
1932
+ type: "string",
1933
+ required: false
1934
+ },
1935
+ /**
1936
+ * Denormalized Role display name — required to compose the SK's
1937
+ * `<normalizedRoleName>` segment. Optional on the schema (pre-TR-024
1938
+ * rows fall back to a sentinel) but expected to be present at write
1939
+ * time per TR-024 rule 2 (write-time source =
1940
+ * canonical Role.displayName).
1941
+ */
1942
+ denormalizedRoleName: {
1943
+ type: "string",
1944
+ required: false
1945
+ }
1946
+ },
1947
+ indexes: {
1948
+ /**
1949
+ * Base table: PK = USER#ID#<userId>, SK = operation-supplied. Both
1950
+ * sub-lanes (tenant-level and workspace-level) use this same index —
1951
+ * the SK string encodes the lane discriminator
1952
+ * (`ROLEASSIGNMENT#TENANT#…` vs `ROLEASSIGNMENT#WORKSPACE#…`) so a
1953
+ * single `Query(PK = USER#ID#<userId>, SK begins_with
1954
+ * 'ROLEASSIGNMENT#')` returns both lanes interleaved.
1955
+ */
1956
+ record: {
1957
+ pk: {
1958
+ field: "PK",
1959
+ composite: ["userId"],
1960
+ template: "USER#ID#${userId}"
1961
+ },
1962
+ sk: {
1963
+ field: "SK",
1964
+ casing: "none",
1965
+ composite: ["sk"],
1966
+ template: "${sk}"
1967
+ }
1968
+ }
1969
+ }
1970
+ });
1971
+
1972
+ // src/data/dynamo/entities/control/roleassignment-workspace-projection-entity.ts
1973
+ var import_electrodb10 = require("electrodb");
1974
+ var RoleAssignmentWorkspaceProjectionEntity = new import_electrodb10.Entity({
1975
+ model: {
1976
+ entity: "roleAssignmentWorkspaceProjection",
1977
+ service: "control",
1978
+ version: "01"
1979
+ },
1980
+ attributes: {
1981
+ /**
1982
+ * Tenant the workspace belongs to. Renders as the leading segment
1983
+ * of the base-table PK. Always required — the workspace partition
1984
+ * is tenant-scoped per ADR-011.
1985
+ */
1986
+ tenantId: {
1987
+ type: "string",
1988
+ required: true
1989
+ },
1990
+ /**
1991
+ * Workspace partition discriminator. Renders as the trailing
1992
+ * segment of the base-table PK
1993
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
1994
+ * the projection has no meaning outside a workspace partition.
1995
+ */
1996
+ workspaceId: {
1997
+ type: "string",
1998
+ required: true
1999
+ },
2000
+ /**
2001
+ * Pre-composed sort key — built by the operations-layer projection
2002
+ * writer via `buildRoleAssignmentWorkspaceProjectionSk`. The entity
2003
+ * stores the value verbatim so the SK grammar (pattern #9) is
2004
+ * owned by the operations layer, not duplicated here.
2005
+ */
2006
+ sk: {
2007
+ type: "string",
2008
+ required: true
2009
+ },
2010
+ /**
2011
+ * User the role assignment grants the role to. Stored as a
2012
+ * discriminating field so consumers can hydrate the canonical User
2013
+ * row via `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
2014
+ * projection's `summary` is insufficient.
2015
+ */
2016
+ userId: {
2017
+ type: "string",
2018
+ required: true
2019
+ },
2020
+ /**
2021
+ * Role the assignment grants. Stored as a discriminating field —
2022
+ * also rendered into the SK as the discriminator-first segment so
2023
+ * `begins_with('ROLEASSIGNMENT#<roleId>#')` filters one role.
2024
+ */
2025
+ roleId: {
2026
+ type: "string",
2027
+ required: true
2028
+ },
2029
+ /**
2030
+ * RoleAssignment canonical-record id. Stored as a discriminating
2031
+ * field so consumers can hydrate the canonical row via
2032
+ * `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
2033
+ * when the projection's `summary` is insufficient.
2034
+ */
2035
+ roleAssignmentId: {
2036
+ type: "string",
2037
+ required: true
2038
+ },
2039
+ /**
2040
+ * Summary projection (key display fields as JSON string: id,
2041
+ * displayName, status) — mirrored from the canonical RoleAssignment
2042
+ * row so workspace-partition queries do not need a BatchGet hop.
2043
+ */
2044
+ summary: {
2045
+ type: "string",
2046
+ required: true
2047
+ },
2048
+ /** Version id mirrored from the canonical RoleAssignment row. */
2049
+ vid: {
2050
+ type: "string",
2051
+ required: true
2052
+ },
2053
+ /** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
2054
+ lastUpdated: {
2055
+ type: "string",
2056
+ required: true
2057
+ },
2058
+ /**
2059
+ * Denormalized User display name — required to compose the
2060
+ * pattern-#9 SK (`ROLEASSIGNMENT#<roleId>#<normalizedUserName>#…`).
2061
+ * Optional on the schema because pre-TR-024 rows may not carry a
2062
+ * display name; the operations layer falls back to a sentinel when
2063
+ * missing so the SK still has a valid shape. The TR-023 rename-
2064
+ * cascade pipeline rewrites the SK on a User rename.
2065
+ */
2066
+ denormalizedUserName: {
2067
+ type: "string",
2068
+ required: false
2069
+ },
2070
+ /**
2071
+ * Denormalized Role display name — mirrored from the canonical
2072
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
2073
+ * Carried on the projection so consumers can render the role's
2074
+ * display name without a hop to the Role record. Not part of the
2075
+ * SK (pattern #9 sorts on `<normalizedUserName>`, not role name) —
2076
+ * a Role rename does NOT rewrite this SK.
2077
+ */
2078
+ denormalizedRoleName: {
2079
+ type: "string",
2080
+ required: false
2081
+ }
2082
+ },
2083
+ indexes: {
2084
+ /**
2085
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
2086
+ * SK = operation-supplied. Pattern #9 uses this index — the SK
2087
+ * encodes the entity-type prefix and discriminator-first roleId
2088
+ * (`ROLEASSIGNMENT#<roleId>#…`) so
2089
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'ROLEASSIGNMENT#<roleId>#')`
2090
+ * returns every user-assignment for that role in the workspace, sorted
2091
+ * by normalized user name.
2092
+ */
2093
+ record: {
2094
+ pk: {
2095
+ field: "PK",
2096
+ composite: ["tenantId", "workspaceId"],
2097
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
2098
+ },
2099
+ sk: {
2100
+ field: "SK",
2101
+ casing: "none",
2102
+ composite: ["sk"],
2103
+ template: "${sk}"
2104
+ }
2105
+ }
2106
+ }
2107
+ });
2108
+
2109
+ // src/data/dynamo/entities/control/tenant-entity.ts
2110
+ var import_electrodb11 = require("electrodb");
2111
+ var TenantEntity = new import_electrodb11.Entity({
2112
+ model: {
2113
+ entity: "tenant",
2114
+ service: "control",
2115
+ version: "01"
2116
+ },
2117
+ attributes: {
2118
+ /** Sort key sentinel. Always "CURRENT". */
2119
+ sk: {
2120
+ type: "string",
2121
+ required: true,
2122
+ default: "CURRENT"
2123
+ },
2124
+ /** The tenant's own id (= resource id). Drives the partition key. */
2125
+ tenantId: {
2126
+ type: "string",
2127
+ required: true
2128
+ },
2129
+ /** FHIR Resource.id; logical id in URL. Equals tenantId. */
2130
+ id: {
2131
+ type: "string",
2132
+ required: true
2133
+ },
2134
+ /** Full Tenant resource serialized as JSON string. */
2135
+ resource: {
2136
+ type: "string",
2137
+ required: true
2138
+ },
2139
+ /**
2140
+ * Summary projection (key display fields as JSON string: id, displayName, status).
2141
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
2142
+ */
2143
+ summary: {
2144
+ type: "string",
2145
+ required: true
2146
+ },
2147
+ /** Version id (e.g. ULID). */
2148
+ vid: {
2149
+ type: "string",
2150
+ required: true
2151
+ },
2152
+ lastUpdated: {
2153
+ type: "string",
2154
+ required: true
2155
+ },
2156
+ gsi1Shard: gsi1ShardAttribute,
2157
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
2158
+ gsi1sk: gsi1skAttribute,
2159
+ deleted: {
2160
+ type: "boolean",
2161
+ required: false
2162
+ },
2163
+ bundleId: {
2164
+ type: "string",
2165
+ required: false
2166
+ },
2167
+ msgId: {
2168
+ type: "string",
2169
+ required: false
2170
+ }
2171
+ },
2172
+ indexes: {
2173
+ /** Base table: PK = TENANT#ID#<tenantId>, SK = CURRENT. Do not supply PK or SK from outside. */
2174
+ record: {
2175
+ pk: {
2176
+ field: "PK",
2177
+ composite: ["tenantId"],
2178
+ template: "TENANT#ID#${tenantId}"
2179
+ },
2180
+ sk: {
2181
+ field: "SK",
2182
+ composite: ["sk"],
2183
+ template: "${sk}"
2184
+ }
2185
+ },
2186
+ /**
2187
+ * GSI1 — Unified Sharded List per ADR-011: list all Tenants across the four shards.
2188
+ * Tenant lives at the platform tier (no parent tenant or workspace), so `TID#-#WID#-`
2189
+ * sentinels precede `RT#Tenant#SHARD#<n>`. SK is derived via `gsi1skAttribute` —
2190
+ * `<normalizedName>#<id>` when the resource carries a `name`, else `<lastUpdated>#<id>`
2191
+ * (DR-004). `casing: "none"` preserves the normalized label and ISO-8601 `T`/`Z`.
2192
+ */
2193
+ gsi1: {
2194
+ index: "GSI1",
2195
+ pk: {
2196
+ field: "GSI1PK",
2197
+ composite: ["gsi1Shard"],
2198
+ template: "TID#-#WID#-#RT#Tenant#SHARD#${gsi1Shard}"
2199
+ },
2200
+ sk: {
2201
+ field: "GSI1SK",
2202
+ casing: "none",
2203
+ composite: ["gsi1sk"],
2204
+ template: "${gsi1sk}"
2205
+ }
2206
+ }
2207
+ }
2208
+ });
2209
+
2210
+ // src/data/dynamo/entities/control/user-entity.ts
2211
+ var import_electrodb12 = require("electrodb");
2212
+ var UserEntity = new import_electrodb12.Entity({
2213
+ model: {
2214
+ entity: "user",
2215
+ service: "control",
2216
+ version: "01"
2217
+ },
2218
+ attributes: {
2219
+ /** Sort key sentinel. Always "CURRENT". */
2220
+ sk: {
2221
+ type: "string",
2222
+ required: true,
2223
+ default: "CURRENT"
2224
+ },
2225
+ /** FHIR Resource.id; platform user id (ohi_uid). */
2226
+ id: {
2227
+ type: "string",
2228
+ required: true
2229
+ },
2230
+ /** Full User resource serialized as JSON string. */
2231
+ resource: {
2232
+ type: "string",
2233
+ required: true
2234
+ },
2235
+ /**
2236
+ * Summary projection (key display fields as JSON string: id, displayName, status).
2237
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
2238
+ */
2239
+ summary: {
2240
+ type: "string",
2241
+ required: true
2242
+ },
2243
+ /**
2244
+ * Immutable Cognito-issued `sub` claim. Drives GSI2 (sub-lookup). Optional until the
2245
+ * Post Confirmation Lambda (#770) lands; required thereafter.
2246
+ */
2247
+ cognitoSub: {
2248
+ type: "string",
2249
+ required: false
2250
+ },
2251
+ /** Version id (e.g. ULID). */
2252
+ vid: {
2253
+ type: "string",
2254
+ required: true
2255
+ },
2256
+ lastUpdated: {
2257
+ type: "string",
2258
+ required: true
2259
+ },
2260
+ gsi1Shard: gsi1ShardAttribute,
2261
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
2262
+ gsi1sk: gsi1skAttribute,
2263
+ deleted: {
2264
+ type: "boolean",
2265
+ required: false
2266
+ },
2267
+ /**
2268
+ * TR-022 / ADR-018 lifecycle state for the cascade pipeline.
2269
+ *
2270
+ * - `active` (or undefined) — normal, readable state.
2271
+ * - `deleting` — intermediate state set synchronously by the
2272
+ * hard-delete API entry point. The owning-delete cascade state
2273
+ * machine fans out from this transition (DynamoDB stream →
2274
+ * `control-plane.owning-delete.v1` → Step Functions). Readers MUST
2275
+ * short-circuit on `deleting` so partial cascades stay invisible.
2276
+ * - `deleted-failed` — terminal failure state set by the cascade
2277
+ * finalize Lambda when the cascade run fails irrecoverably.
2278
+ * Operators recover by re-running the cascade or by direct
2279
+ * intervention.
2280
+ *
2281
+ * The cascade finalize step deletes the canonical record conditional
2282
+ * on `lifecycleState = "deleting"`; on replay the conditional check
2283
+ * fails and the finalize step treats that as a no-op success.
2284
+ */
2285
+ lifecycleState: {
2286
+ type: ["active", "deleting", "deleted-failed"],
2287
+ required: false
2288
+ },
2289
+ bundleId: {
2290
+ type: "string",
2291
+ required: false
2292
+ },
2293
+ msgId: {
2294
+ type: "string",
2295
+ required: false
2296
+ }
2297
+ },
2298
+ indexes: {
2299
+ /** Base table: PK = USER#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
2300
+ record: {
2301
+ pk: {
2302
+ field: "PK",
2303
+ composite: ["id"],
2304
+ template: "USER#ID#${id}"
2305
+ },
2306
+ sk: {
2307
+ field: "SK",
2308
+ composite: ["sk"],
2309
+ template: "${sk}"
2310
+ }
2311
+ },
2312
+ /**
2313
+ * GSI1 — Unified Sharded List per ADR-011: list all Users across the four shards.
2314
+ * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#User#SHARD#<n>`.
2315
+ * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
2316
+ * extractable (string `name`/`title` via introspection), else `<lastUpdated>#<id>`
2317
+ * (DR-004). `casing: "none"` preserves the normalized label and ISO-8601 `T`/`Z`.
2318
+ */
2319
+ gsi1: {
2320
+ index: "GSI1",
2321
+ pk: {
2322
+ field: "GSI1PK",
2323
+ composite: ["gsi1Shard"],
2324
+ template: "TID#-#WID#-#RT#User#SHARD#${gsi1Shard}"
2325
+ },
2326
+ sk: {
2327
+ field: "GSI1SK",
2328
+ casing: "none",
2329
+ composite: ["gsi1sk"],
2330
+ template: "${gsi1sk}"
2331
+ }
2332
+ },
2333
+ /**
2334
+ * GSI2 — Cognito sub-lookup per ADR-011: resolves the UserEntity from a Cognito `sub` claim.
2335
+ * `condition` skips the index when `cognitoSub` is missing so legacy items without a sub are
2336
+ * not indexed.
2337
+ */
2338
+ gsi2: {
2339
+ index: "GSI2",
2340
+ condition: (attrs) => typeof attrs.cognitoSub === "string" && attrs.cognitoSub.length > 0,
2341
+ pk: {
2342
+ field: "GSI2PK",
2343
+ casing: "none",
2344
+ composite: ["cognitoSub"],
2345
+ template: "USER#SUB#${cognitoSub}"
2346
+ },
2347
+ sk: {
2348
+ field: "GSI2SK",
2349
+ casing: "none",
2350
+ composite: [],
2351
+ template: "CURRENT"
2352
+ }
2353
+ }
2354
+ }
2355
+ });
2356
+
2357
+ // src/data/dynamo/entities/control/workspace-entity.ts
2358
+ var import_electrodb13 = require("electrodb");
2359
+ var WorkspaceEntity = new import_electrodb13.Entity({
2360
+ model: {
2361
+ entity: "workspace",
2362
+ service: "control",
2363
+ version: "01"
2364
+ },
2365
+ attributes: {
2366
+ /** Sort key sentinel. Always "CURRENT". */
2367
+ sk: {
2368
+ type: "string",
2369
+ required: true,
2370
+ default: "CURRENT"
2371
+ },
2372
+ /** Tenant that contains this workspace (required). */
2373
+ tenantId: {
2374
+ type: "string",
2375
+ required: true
2376
+ },
2377
+ /** FHIR Resource.id; logical id in URL. */
2378
+ id: {
2379
+ type: "string",
2380
+ required: true
2381
+ },
2382
+ /** Full Workspace resource serialized as JSON string. */
2383
+ resource: {
2384
+ type: "string",
2385
+ required: true
2386
+ },
2387
+ /**
2388
+ * Summary projection (key display fields as JSON string: id, displayName, status).
2389
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
2390
+ */
2391
+ summary: {
2392
+ type: "string",
2393
+ required: true
2394
+ },
2395
+ /** Version id (e.g. ULID). */
2396
+ vid: {
2397
+ type: "string",
2398
+ required: true
2399
+ },
2400
+ lastUpdated: {
2401
+ type: "string",
2402
+ required: true
2403
+ },
2404
+ gsi1Shard: gsi1ShardAttribute,
2405
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
2406
+ gsi1sk: gsi1skAttribute,
2407
+ deleted: {
2408
+ type: "boolean",
2409
+ required: false
2410
+ },
2411
+ /**
2412
+ * TR-022 / ADR-018 lifecycle state for the cascade pipeline.
2413
+ *
2414
+ * - `active` (or undefined) — normal, readable state.
2415
+ * - `deleting` — intermediate state set synchronously by the
2416
+ * hard-delete API entry point. The owning-delete cascade state
2417
+ * machine fans out from this transition (DynamoDB stream →
2418
+ * `control-plane.owning-delete.v1` → Step Functions). Readers MUST
2419
+ * short-circuit on `deleting` so partial cascades stay invisible.
2420
+ * - `deleted-failed` — terminal failure state set by the cascade
2421
+ * finalize Lambda when the cascade run fails irrecoverably.
2422
+ * Operators recover by re-running the cascade or by direct
2423
+ * intervention.
2424
+ *
2425
+ * The cascade finalize step deletes the canonical record conditional
2426
+ * on `lifecycleState = "deleting"`; on replay the conditional check
2427
+ * fails and the finalize step treats that as a no-op success.
2428
+ */
2429
+ lifecycleState: {
2430
+ type: ["active", "deleting", "deleted-failed"],
2431
+ required: false
2432
+ },
2433
+ bundleId: {
2434
+ type: "string",
2435
+ required: false
2436
+ },
2437
+ msgId: {
2438
+ type: "string",
2439
+ required: false
2440
+ }
2441
+ },
2442
+ indexes: {
2443
+ /** Base table: PK = TID#<tenantId>#WORKSPACE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
2444
+ record: {
2445
+ pk: {
2446
+ field: "PK",
2447
+ composite: ["tenantId", "id"],
2448
+ template: "TID#${tenantId}#WORKSPACE#ID#${id}"
2449
+ },
2450
+ sk: {
2451
+ field: "SK",
2452
+ composite: ["sk"],
2453
+ template: "${sk}"
2454
+ }
2455
+ },
2456
+ /**
2457
+ * GSI1 — Unified Sharded List per ADR-011: list all Workspaces for a tenant across the
2458
+ * four shards. Workspace is itself the workspace identity, so `WID#-` is a sentinel.
2459
+ * SK is derived via `gsi1skAttribute` — `<normalizedName>#<id>` when the resource
2460
+ * carries a `name`, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves
2461
+ * the normalized label and ISO-8601 `T`/`Z`.
2462
+ */
2463
+ gsi1: {
2464
+ index: "GSI1",
2465
+ pk: {
2466
+ field: "GSI1PK",
2467
+ composite: ["tenantId", "gsi1Shard"],
2468
+ template: "TID#${tenantId}#WID#-#RT#Workspace#SHARD#${gsi1Shard}"
2469
+ },
2470
+ sk: {
2471
+ field: "GSI1SK",
2472
+ casing: "none",
2473
+ composite: ["gsi1sk"],
2474
+ template: "${gsi1sk}"
2475
+ }
2476
+ }
2477
+ }
2478
+ });
2479
+
2480
+ // src/data/dynamo/dynamo-control-service.ts
2481
+ var controlPlaneEntities = {
2482
+ configuration: ConfigurationEntity,
2483
+ configurationUserProjection: ConfigurationUserProjectionEntity,
2484
+ configurationWorkspaceProjection: ConfigurationWorkspaceProjectionEntity,
2485
+ membership: MembershipEntity,
2486
+ membershipUserProjection: MembershipUserProjectionEntity,
2487
+ membershipWorkspaceProjection: MembershipWorkspaceProjectionEntity,
2488
+ role: RoleEntity,
2489
+ roleAssignment: RoleAssignmentEntity,
2490
+ roleAssignmentUserProjection: RoleAssignmentUserProjectionEntity,
2491
+ roleAssignmentWorkspaceProjection: RoleAssignmentWorkspaceProjectionEntity,
2492
+ tenant: TenantEntity,
2493
+ user: UserEntity,
2494
+ workspace: WorkspaceEntity
2495
+ };
2496
+ var controlPlaneService = new import_electrodb14.Service(controlPlaneEntities, {
2497
+ table: defaultTableName,
2498
+ client: dynamoClient
2499
+ });
2500
+ var DynamoControlService = {
2501
+ entities: controlPlaneService.entities,
2502
+ transaction: controlPlaneService.transaction
2503
+ };
2504
+ function getDynamoControlService(tableName) {
2505
+ const resolved = tableName ?? defaultTableName;
2506
+ const service = new import_electrodb14.Service(controlPlaneEntities, {
2507
+ table: resolved,
2508
+ client: dynamoClient
2509
+ });
2510
+ return {
2511
+ entities: service.entities,
2512
+ transaction: service.transaction
2513
+ };
2514
+ }
2515
+
2516
+ // src/data/errors/domain-errors.ts
2517
+ var DomainError = class extends Error {
2518
+ constructor(message, code, options) {
2519
+ super(message, options);
2520
+ this.name = this.constructor.name;
2521
+ this.code = code;
2522
+ this.details = options?.details;
2523
+ Object.setPrototypeOf(this, new.target.prototype);
2524
+ }
2525
+ };
2526
+ var ValidationError = class extends DomainError {
2527
+ constructor(message, options) {
2528
+ super(message, "VALIDATION", options);
2529
+ }
2530
+ };
2531
+ var ConflictError = class extends DomainError {
2532
+ constructor(message, options) {
2533
+ super(message, "CONFLICT", options);
2534
+ }
2535
+ };
2536
+
2537
+ // src/data/operations/control/multi-write-operation.ts
2538
+ var TRANSACT_WRITE_ITEM_LIMIT = 100;
2539
+ async function executeMultiWrite(params) {
2540
+ const { service, triples, token } = params;
2541
+ if (triples.length === 0) {
2542
+ throw new ValidationError(
2543
+ "executeMultiWrite called with zero triples; at least one triple is required"
2544
+ );
2545
+ }
2546
+ if (triples.length > TRANSACT_WRITE_ITEM_LIMIT) {
2547
+ throw new ValidationError(
2548
+ `executeMultiWrite received ${triples.length} triples; DynamoDB TransactWriteItems is limited to ${TRANSACT_WRITE_ITEM_LIMIT} items per call`,
2549
+ {
2550
+ details: {
2551
+ itemsRequested: triples.length,
2552
+ limit: TRANSACT_WRITE_ITEM_LIMIT
2553
+ }
2554
+ }
2555
+ );
2556
+ }
2557
+ for (const [index, triple] of triples.entries()) {
2558
+ if (!triple || typeof triple !== "object") {
2559
+ throw new ValidationError(
2560
+ `executeMultiWrite triple at index ${index} is not an object`
2561
+ );
2562
+ }
2563
+ if (typeof triple.entity !== "string" || triple.entity.length === 0) {
2564
+ throw new ValidationError(
2565
+ `executeMultiWrite triple at index ${index} is missing a non-empty 'entity' key`
2566
+ );
2567
+ }
2568
+ if (!isSupportedAction(triple.action)) {
2569
+ throw new ValidationError(
2570
+ `executeMultiWrite triple at index ${index} has unsupported action '${String(
2571
+ triple.action
2572
+ )}'; supported: 'put' | 'create' | 'delete'`
2573
+ );
2574
+ }
2575
+ if (!triple.item || typeof triple.item !== "object") {
2576
+ throw new ValidationError(
2577
+ `executeMultiWrite triple at index ${index} is missing an 'item' payload`
2578
+ );
2579
+ }
2580
+ }
2581
+ let result;
2582
+ try {
2583
+ result = await service.transaction.write(
2584
+ (entities) => triples.map((triple, index) => {
2585
+ const transactEntity = entities[triple.entity];
2586
+ if (transactEntity === void 0) {
2587
+ throw new ValidationError(
2588
+ `executeMultiWrite triple at index ${index} references unknown entity '${triple.entity}'; ensure the service exposes it`
2589
+ );
2590
+ }
2591
+ switch (triple.action) {
2592
+ case "put":
2593
+ return transactEntity.put(triple.item).commit();
2594
+ case "create":
2595
+ return transactEntity.create(triple.item).commit();
2596
+ case "delete":
2597
+ return transactEntity.delete(triple.item).commit();
2598
+ default:
2599
+ throw new ValidationError(
2600
+ `executeMultiWrite triple at index ${index} has unsupported action '${String(
2601
+ triple.action
2602
+ )}'`
2603
+ );
2604
+ }
2605
+ })
2606
+ ).go(token === void 0 ? void 0 : { token });
2607
+ } catch (err) {
2608
+ if (err instanceof DomainError) {
2609
+ throw err;
2610
+ }
2611
+ throw new ConflictError(buildCancellationMessage(err), {
2612
+ cause: err,
2613
+ details: extractCancellationReasons(err)
2614
+ });
2615
+ }
2616
+ if (result.canceled) {
2617
+ throw new ConflictError(
2618
+ "TransactWriteItems was canceled by DynamoDB (check CancellationReasons on the cause for details)",
2619
+ { details: { canceled: true, data: result.data } }
2620
+ );
2621
+ }
2622
+ return { itemsWritten: triples.length, canceled: false };
2623
+ }
2624
+ function isSupportedAction(value) {
2625
+ return value === "put" || value === "create" || value === "delete";
2626
+ }
2627
+ function buildCancellationMessage(err) {
2628
+ if (err instanceof Error && err.message) {
2629
+ return `TransactWriteItems failed: ${err.message}`;
2630
+ }
2631
+ return "TransactWriteItems failed (no error message available)";
2632
+ }
2633
+ function extractCancellationReasons(err) {
2634
+ if (err && typeof err === "object") {
2635
+ const cancellationReasons = err.CancellationReasons;
2636
+ if (cancellationReasons !== void 0) {
2637
+ return { CancellationReasons: cancellationReasons };
2638
+ }
2639
+ }
2640
+ return void 0;
2641
+ }
2642
+
2643
+ // src/data/operations/control/owning-delete/owning-delete-batch-delete-operation.ts
2644
+ var OWNING_DELETE_CASCADE_CHUNK_SIZE = TRANSACT_WRITE_ITEM_LIMIT;
2645
+ async function deleteOwningChildChunkOperation(params) {
2646
+ const { rows, tableName, token } = params;
2647
+ if (rows.length === 0) {
2648
+ return { rowsDeleted: 0, breakdown: {} };
2649
+ }
2650
+ if (rows.length > OWNING_DELETE_CASCADE_CHUNK_SIZE) {
2651
+ throw new Error(
2652
+ `deleteOwningChildChunkOperation: chunk has ${rows.length} rows; limit is ${OWNING_DELETE_CASCADE_CHUNK_SIZE}`
2653
+ );
2654
+ }
2655
+ const service = getDynamoControlService(tableName);
2656
+ const triples = rows.map((row) => ({
2657
+ entity: row.entity,
2658
+ action: "delete",
2659
+ item: { ...row.key }
2660
+ }));
2661
+ await executeMultiWrite({ service, triples, token });
2662
+ const breakdown = {};
2663
+ for (const row of rows) {
2664
+ breakdown[row.entity] = (breakdown[row.entity] ?? 0) + 1;
2665
+ }
2666
+ return { rowsDeleted: rows.length, breakdown };
2667
+ }
2668
+
2669
+ // src/data/operations/control/owning-delete/owning-delete-list-child-projections-operation.ts
2670
+ var import_workflows = __toESM(require_lib());
2671
+ var OWNING_DELETE_PROJECTION_ENTITY = {
2672
+ MembershipUserProjection: "membershipUserProjection",
2673
+ MembershipWorkspaceProjection: "membershipWorkspaceProjection",
2674
+ RoleAssignmentUserProjection: "roleAssignmentUserProjection",
2675
+ RoleAssignmentWorkspaceProjection: "roleAssignmentWorkspaceProjection",
2676
+ ConfigurationUserProjection: "configurationUserProjection",
2677
+ ConfigurationWorkspaceProjection: "configurationWorkspaceProjection"
2678
+ };
2679
+
2680
+ // src/workflows/control-plane/owning-delete-cascade/delete-chunk.handler.ts
2681
+ function isKnownEntity(value) {
2682
+ for (const known of Object.values(OWNING_DELETE_PROJECTION_ENTITY)) {
2683
+ if (known === value) {
2684
+ return true;
2685
+ }
2686
+ }
2687
+ return false;
2688
+ }
2689
+ var handler = async (input) => {
2690
+ const rows = input.rows.map((row) => {
2691
+ if (!isKnownEntity(row.entity)) {
2692
+ throw new Error(
2693
+ `owning-delete cascade delete-chunk: unknown projection entity '${row.entity}'`
2694
+ );
2695
+ }
2696
+ return {
2697
+ entity: row.entity,
2698
+ key: { ...row.key }
2699
+ };
2700
+ });
2701
+ const result = await deleteOwningChildChunkOperation({
2702
+ rows,
2703
+ token: input.chunkToken
2704
+ });
2705
+ return {
2706
+ ownerType: input.ownerType,
2707
+ ownerId: input.ownerId,
2708
+ tenantId: input.tenantId,
2709
+ rowsDeleted: result.rowsDeleted
2710
+ };
2711
+ };
2712
+ // Annotate the CommonJS export names for ESM import in node:
2713
+ 0 && (module.exports = {
2714
+ handler
2715
+ });
2716
+ //# sourceMappingURL=delete-chunk.handler.js.map