@openhi/constructs 0.0.110 → 0.0.112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/lib/chunk-23PUSHBV.mjs +24 -0
  2. package/lib/chunk-23PUSHBV.mjs.map +1 -0
  3. package/lib/chunk-2O3CXY2C.mjs +79 -0
  4. package/lib/chunk-2O3CXY2C.mjs.map +1 -0
  5. package/lib/{chunk-7FUAMZOF.mjs → chunk-53OHXLIL.mjs} +3 -3
  6. package/lib/chunk-6NBGYGFL.mjs +1803 -0
  7. package/lib/chunk-6NBGYGFL.mjs.map +1 -0
  8. package/lib/chunk-7RZHFI77.mjs +22 -0
  9. package/lib/chunk-7RZHFI77.mjs.map +1 -0
  10. package/lib/{chunk-7Q2IJ2J5.mjs → chunk-CUUKXDB2.mjs} +6 -6
  11. package/lib/chunk-FYHBHHWK.mjs +47 -0
  12. package/lib/chunk-FYHBHHWK.mjs.map +1 -0
  13. package/lib/{chunk-MULKGFIJ.mjs → chunk-GBDIGTNV.mjs} +165 -10
  14. package/lib/chunk-GBDIGTNV.mjs.map +1 -0
  15. package/lib/chunk-HQ67J7BP.mjs +199 -0
  16. package/lib/chunk-HQ67J7BP.mjs.map +1 -0
  17. package/lib/{chunk-AJ3G3THO.mjs → chunk-KO64HPWQ.mjs} +2 -2
  18. package/lib/{chunk-BB5MK4L3.mjs → chunk-KSFC72TT.mjs} +3 -3
  19. package/lib/{chunk-2TPJ6HOF.mjs → chunk-NZRW7ROK.mjs} +72 -54
  20. package/lib/chunk-NZRW7ROK.mjs.map +1 -0
  21. package/lib/chunk-QJDHVMKT.mjs +117 -0
  22. package/lib/chunk-QJDHVMKT.mjs.map +1 -0
  23. package/lib/{chunk-IS4VQRI4.mjs → chunk-QMBJ4VHC.mjs} +12 -47
  24. package/lib/chunk-QMBJ4VHC.mjs.map +1 -0
  25. package/lib/chunk-TRY7JGWO.mjs +16 -0
  26. package/lib/chunk-TRY7JGWO.mjs.map +1 -0
  27. package/lib/chunk-W4KR4CSL.mjs +236 -0
  28. package/lib/chunk-W4KR4CSL.mjs.map +1 -0
  29. package/lib/{chunk-AGF3RAAZ.mjs → chunk-WPCBVDFZ.mjs} +2 -2
  30. package/lib/chunk-WQWFVEVX.mjs +66 -0
  31. package/lib/chunk-WQWFVEVX.mjs.map +1 -0
  32. package/lib/{chunk-SYBADQXI.mjs → chunk-ZM4GDHHC.mjs} +77 -2
  33. package/lib/chunk-ZM4GDHHC.mjs.map +1 -0
  34. package/lib/data-store-postgres-replication.handler.js +26 -17
  35. package/lib/data-store-postgres-replication.handler.js.map +1 -1
  36. package/lib/data-store-postgres-replication.handler.mjs +5 -65
  37. package/lib/data-store-postgres-replication.handler.mjs.map +1 -1
  38. package/lib/delete-chunk.handler.d.mts +29 -0
  39. package/lib/delete-chunk.handler.d.ts +29 -0
  40. package/lib/delete-chunk.handler.js +2716 -0
  41. package/lib/delete-chunk.handler.js.map +1 -0
  42. package/lib/delete-chunk.handler.mjs +47 -0
  43. package/lib/delete-chunk.handler.mjs.map +1 -0
  44. package/lib/events-CjS-sm0W.d.mts +107 -0
  45. package/lib/events-CjS-sm0W.d.ts +107 -0
  46. package/lib/events-Da_cFgtc.d.mts +208 -0
  47. package/lib/events-Da_cFgtc.d.ts +208 -0
  48. package/lib/finalize.handler.d.mts +35 -0
  49. package/lib/finalize.handler.d.ts +35 -0
  50. package/lib/finalize.handler.js +875 -0
  51. package/lib/finalize.handler.js.map +1 -0
  52. package/lib/finalize.handler.mjs +166 -0
  53. package/lib/finalize.handler.mjs.map +1 -0
  54. package/lib/index.d.mts +189 -2
  55. package/lib/index.d.ts +500 -3
  56. package/lib/index.js +1753 -174
  57. package/lib/index.js.map +1 -1
  58. package/lib/index.mjs +571 -17
  59. package/lib/index.mjs.map +1 -1
  60. package/lib/list-chunks.handler.d.mts +28 -0
  61. package/lib/list-chunks.handler.d.ts +28 -0
  62. package/lib/list-chunks.handler.js +2746 -0
  63. package/lib/list-chunks.handler.js.map +1 -0
  64. package/lib/list-chunks.handler.mjs +54 -0
  65. package/lib/list-chunks.handler.mjs.map +1 -0
  66. package/lib/platform-deploy-bridge.handler.js +76 -1
  67. package/lib/platform-deploy-bridge.handler.js.map +1 -1
  68. package/lib/platform-deploy-bridge.handler.mjs +1 -1
  69. package/lib/pre-token-generation.handler.js +1106 -155
  70. package/lib/pre-token-generation.handler.js.map +1 -1
  71. package/lib/pre-token-generation.handler.mjs +6 -4
  72. package/lib/pre-token-generation.handler.mjs.map +1 -1
  73. package/lib/provision-default-workspace.handler.js +1529 -142
  74. package/lib/provision-default-workspace.handler.js.map +1 -1
  75. package/lib/provision-default-workspace.handler.mjs +8 -4
  76. package/lib/provision-default-workspace.handler.mjs.map +1 -1
  77. package/lib/rename-finalize.handler.d.mts +30 -0
  78. package/lib/rename-finalize.handler.d.ts +30 -0
  79. package/lib/rename-finalize.handler.js +795 -0
  80. package/lib/rename-finalize.handler.js.map +1 -0
  81. package/lib/rename-finalize.handler.mjs +90 -0
  82. package/lib/rename-finalize.handler.mjs.map +1 -0
  83. package/lib/rename-list-targets.handler.d.mts +26 -0
  84. package/lib/rename-list-targets.handler.d.ts +26 -0
  85. package/lib/rename-list-targets.handler.js +2985 -0
  86. package/lib/rename-list-targets.handler.js.map +1 -0
  87. package/lib/rename-list-targets.handler.mjs +431 -0
  88. package/lib/rename-list-targets.handler.mjs.map +1 -0
  89. package/lib/rename-rewrite-chunk.handler.d.mts +35 -0
  90. package/lib/rename-rewrite-chunk.handler.d.ts +35 -0
  91. package/lib/rename-rewrite-chunk.handler.js +2021 -0
  92. package/lib/rename-rewrite-chunk.handler.js.map +1 -0
  93. package/lib/rename-rewrite-chunk.handler.mjs +27 -0
  94. package/lib/rename-rewrite-chunk.handler.mjs.map +1 -0
  95. package/lib/rest-api-lambda.handler.js +4087 -921
  96. package/lib/rest-api-lambda.handler.js.map +1 -1
  97. package/lib/rest-api-lambda.handler.mjs +1827 -81
  98. package/lib/rest-api-lambda.handler.mjs.map +1 -1
  99. package/lib/seed-demo-data.handler.js +1588 -124
  100. package/lib/seed-demo-data.handler.js.map +1 -1
  101. package/lib/seed-demo-data.handler.mjs +10 -6
  102. package/lib/seed-system-data.handler.js +1179 -155
  103. package/lib/seed-system-data.handler.js.map +1 -1
  104. package/lib/seed-system-data.handler.mjs +5 -4
  105. package/lib/seed-system-data.handler.mjs.map +1 -1
  106. package/package.json +1 -1
  107. package/lib/chunk-2TPJ6HOF.mjs.map +0 -1
  108. package/lib/chunk-IS4VQRI4.mjs.map +0 -1
  109. package/lib/chunk-MULKGFIJ.mjs.map +0 -1
  110. package/lib/chunk-QR5JVSCF.mjs +0 -862
  111. package/lib/chunk-QR5JVSCF.mjs.map +0 -1
  112. package/lib/chunk-SYBADQXI.mjs.map +0 -1
  113. /package/lib/{chunk-7FUAMZOF.mjs.map → chunk-53OHXLIL.mjs.map} +0 -0
  114. /package/lib/{chunk-7Q2IJ2J5.mjs.map → chunk-CUUKXDB2.mjs.map} +0 -0
  115. /package/lib/{chunk-AJ3G3THO.mjs.map → chunk-KO64HPWQ.mjs.map} +0 -0
  116. /package/lib/{chunk-BB5MK4L3.mjs.map → chunk-KSFC72TT.mjs.map} +0 -0
  117. /package/lib/{chunk-AGF3RAAZ.mjs.map → chunk-WPCBVDFZ.mjs.map} +0 -0
@@ -134,6 +134,56 @@ var require_registry = __commonJS({
134
134
  }
135
135
  });
136
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
+
137
187
  // ../workflows/lib/detail-types/platform.js
138
188
  var require_platform = __commonJS({
139
189
  "../workflows/lib/detail-types/platform.js"(exports2) {
@@ -176,6 +226,7 @@ var require_detail_types = __commonJS({
176
226
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
177
227
  };
178
228
  Object.defineProperty(exports2, "__esModule", { value: true });
229
+ __exportStar(require_control_plane(), exports2);
179
230
  __exportStar(require_platform(), exports2);
180
231
  __exportStar(require_registry(), exports2);
181
232
  }
@@ -527,7 +578,7 @@ var require_lib = __commonJS({
527
578
  "../workflows/lib/index.js"(exports2) {
528
579
  "use strict";
529
580
  Object.defineProperty(exports2, "__esModule", { value: true });
530
- 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.PlatformSystemDataSeededV1 = exports2.PlatformDeploymentCompletedV1 = exports2.InvalidDetailTypeRegistrationError = 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;
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;
531
582
  var envelope_version_1 = require_envelope_version();
532
583
  Object.defineProperty(exports2, "ENVELOPE_VERSION", { enumerable: true, get: function() {
533
584
  return envelope_version_1.ENVELOPE_VERSION;
@@ -562,15 +613,39 @@ var require_lib = __commonJS({
562
613
  return sources_1.OPENHI_OPS_SOURCE;
563
614
  } });
564
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
+ } });
565
634
  Object.defineProperty(exports2, "InvalidDetailTypeRegistrationError", { enumerable: true, get: function() {
566
635
  return detail_types_1.InvalidDetailTypeRegistrationError;
567
636
  } });
637
+ Object.defineProperty(exports2, "OWNING_ENTITY_TYPE", { enumerable: true, get: function() {
638
+ return detail_types_1.OWNING_ENTITY_TYPE;
639
+ } });
568
640
  Object.defineProperty(exports2, "PlatformDeploymentCompletedV1", { enumerable: true, get: function() {
569
641
  return detail_types_1.PlatformDeploymentCompletedV1;
570
642
  } });
571
643
  Object.defineProperty(exports2, "PlatformSystemDataSeededV1", { enumerable: true, get: function() {
572
644
  return detail_types_1.PlatformSystemDataSeededV1;
573
645
  } });
646
+ Object.defineProperty(exports2, "RENAMABLE_ENTITY_TYPE", { enumerable: true, get: function() {
647
+ return detail_types_1.RENAMABLE_ENTITY_TYPE;
648
+ } });
574
649
  Object.defineProperty(exports2, "defineDetailType", { enumerable: true, get: function() {
575
650
  return detail_types_1.defineDetailType;
576
651
  } });
@@ -642,7 +717,7 @@ module.exports = __toCommonJS(seed_demo_data_handler_exports);
642
717
  var import_node_crypto = require("crypto");
643
718
  var import_client_cognito_identity_provider = require("@aws-sdk/client-cognito-identity-provider");
644
719
  var import_client_dynamodb2 = require("@aws-sdk/client-dynamodb");
645
- var import_types8 = require("@openhi/types");
720
+ var import_types12 = require("@openhi/types");
646
721
  var import_workflows2 = __toESM(require_lib());
647
722
 
648
723
  // src/workflows/control-plane/seed-demo-data/events.ts
@@ -739,7 +814,7 @@ var demoRolesForUserInTenant = (_user, _tenantId) => {
739
814
  };
740
815
 
741
816
  // src/data/dynamo/dynamo-control-service.ts
742
- var import_electrodb8 = require("electrodb");
817
+ var import_electrodb14 = require("electrodb");
743
818
 
744
819
  // src/data/dynamo/dynamo-client.ts
745
820
  var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
@@ -803,6 +878,60 @@ var gsi1skAttribute = {
803
878
  return label !== void 0 ? `${label}#${id}` : fallback;
804
879
  }
805
880
  };
881
+ function extractRoleId(resource) {
882
+ const flat = resource.roleId;
883
+ if (typeof flat === "string" && flat.length > 0) return flat;
884
+ const role = resource.role;
885
+ if (role && typeof role === "object") {
886
+ const reference = role.reference;
887
+ if (typeof reference === "string" && reference.length > 0) {
888
+ const slash = reference.lastIndexOf("/");
889
+ const tail = slash >= 0 ? reference.slice(slash + 1) : reference;
890
+ if (tail.length > 0) return tail;
891
+ }
892
+ }
893
+ return void 0;
894
+ }
895
+ var roleAssignmentGsi1skAttribute = {
896
+ type: "string",
897
+ watch: ["resource", "denormalizedUserName", "lastUpdated", "id"],
898
+ set: (_val, item) => {
899
+ const id = typeof item?.id === "string" ? item.id : "";
900
+ const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
901
+ const fallback = `${lastUpdated}#${id}`;
902
+ if (typeof item?.resource !== "string" || item.resource.length === 0) {
903
+ return fallback;
904
+ }
905
+ let parsed;
906
+ try {
907
+ parsed = JSON.parse(item.resource);
908
+ } catch {
909
+ return fallback;
910
+ }
911
+ if (!parsed || typeof parsed !== "object") return fallback;
912
+ const roleId = extractRoleId(parsed);
913
+ if (roleId === void 0) return fallback;
914
+ const denormalizedUserName = typeof item.denormalizedUserName === "string" ? item.denormalizedUserName : "";
915
+ const normalizedUserName = denormalizedUserName.length > 0 ? (0, import_types2.normalizeLabel)(denormalizedUserName) : "";
916
+ if (normalizedUserName.length === 0) return fallback;
917
+ return `${roleId}#${normalizedUserName}#${id}`;
918
+ }
919
+ };
920
+ var membershipGsi1skAttribute = {
921
+ type: "string",
922
+ watch: ["denormalizedUserName", "lastUpdated", "id"],
923
+ set: (_val, item) => {
924
+ const id = typeof item?.id === "string" ? item.id : "";
925
+ const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
926
+ const fallback = `${lastUpdated}#${id}`;
927
+ const denormalizedUserName = typeof item?.denormalizedUserName === "string" ? item.denormalizedUserName : "";
928
+ const normalizedUserName = denormalizedUserName.length > 0 ? (0, import_types2.normalizeLabel)(denormalizedUserName) : "";
929
+ if (normalizedUserName.length === 0) {
930
+ return fallback;
931
+ }
932
+ return `${normalizedUserName}#${id}`;
933
+ }
934
+ };
806
935
 
807
936
  // src/data/dynamo/entities/control/configuration-entity.ts
808
937
  var ConfigurationEntity = new import_electrodb.Entity({
@@ -929,9 +1058,239 @@ var ConfigurationEntity = new import_electrodb.Entity({
929
1058
  }
930
1059
  });
931
1060
 
932
- // src/data/dynamo/entities/control/membership-entity.ts
1061
+ // src/data/dynamo/entities/control/configuration-user-projection-entity.ts
933
1062
  var import_electrodb2 = require("electrodb");
934
- var MembershipEntity = new import_electrodb2.Entity({
1063
+ var ConfigurationUserProjectionEntity = new import_electrodb2.Entity({
1064
+ model: {
1065
+ entity: "configurationUserProjection",
1066
+ service: "control",
1067
+ version: "01"
1068
+ },
1069
+ attributes: {
1070
+ /**
1071
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
1072
+ * base-table PK. Always required — the projection has no meaning
1073
+ * outside a user partition.
1074
+ */
1075
+ userId: {
1076
+ type: "string",
1077
+ required: true
1078
+ },
1079
+ /**
1080
+ * Pre-composed sort key — built by the operations-layer projection
1081
+ * writer via `buildConfigurationUserProjectionSk`. The entity stores
1082
+ * the value verbatim so the SK grammar (pattern #10 user-scope) is
1083
+ * owned by the operations layer, not duplicated here.
1084
+ */
1085
+ sk: {
1086
+ type: "string",
1087
+ required: true
1088
+ },
1089
+ /**
1090
+ * Configuration canonical-record id. Stored as a discriminating
1091
+ * field so consumers can hydrate the canonical row via the
1092
+ * Configuration get-by-id operation when the projection's `summary`
1093
+ * is insufficient.
1094
+ */
1095
+ configurationId: {
1096
+ type: "string",
1097
+ required: true
1098
+ },
1099
+ /**
1100
+ * Tenant the Configuration is associated with. The canonical row
1101
+ * keys off `(tenantId, workspaceId, userId, roleId)`; the projection
1102
+ * carries `tenantId` so consumers reconstructing the canonical PK
1103
+ * have the tenant segment without a hop.
1104
+ */
1105
+ tenantId: {
1106
+ type: "string",
1107
+ required: true
1108
+ },
1109
+ /**
1110
+ * Scope marker. Always `"user"` on this projection — recorded
1111
+ * explicitly so future scope-bearing projections (workspace,
1112
+ * tenant, role) can share filter semantics in a unified
1113
+ * cross-projection list query if one ever lands.
1114
+ */
1115
+ scope: {
1116
+ type: "string",
1117
+ required: true,
1118
+ default: "user"
1119
+ },
1120
+ /**
1121
+ * Configuration's `key` attribute (config category, e.g. endpoints,
1122
+ * branding, display). Mirrored from the canonical row so consumers
1123
+ * reading the projection get the natural display label without a
1124
+ * BatchGet hop. Doubles as the source of `<normalizedConfigName>` in
1125
+ * the SK.
1126
+ */
1127
+ displayName: {
1128
+ type: "string",
1129
+ required: false
1130
+ },
1131
+ /**
1132
+ * Summary projection (key display fields as JSON string) — mirrored
1133
+ * from the canonical Configuration row so user-partition queries do
1134
+ * not need a BatchGet hop.
1135
+ */
1136
+ summary: {
1137
+ type: "string",
1138
+ required: true
1139
+ },
1140
+ /** Version id mirrored from the canonical Configuration row. */
1141
+ vid: {
1142
+ type: "string",
1143
+ required: true
1144
+ },
1145
+ /** Last-updated timestamp mirrored from the canonical Configuration row. */
1146
+ lastUpdated: {
1147
+ type: "string",
1148
+ required: true
1149
+ }
1150
+ },
1151
+ indexes: {
1152
+ /**
1153
+ * Base table: PK = USER#ID#<userId>, SK = operation-supplied. A
1154
+ * single `Query(PK = USER#ID#<userId>, SK begins_with
1155
+ * 'CONFIGURATION#')` returns the user's user-scoped Configurations
1156
+ * sorted by `<normalizedConfigName>` (then `<configurationId>` as
1157
+ * the tiebreaker).
1158
+ */
1159
+ record: {
1160
+ pk: {
1161
+ field: "PK",
1162
+ composite: ["userId"],
1163
+ template: "USER#ID#${userId}"
1164
+ },
1165
+ sk: {
1166
+ field: "SK",
1167
+ casing: "none",
1168
+ composite: ["sk"],
1169
+ template: "${sk}"
1170
+ }
1171
+ }
1172
+ }
1173
+ });
1174
+
1175
+ // src/data/dynamo/entities/control/configuration-workspace-projection-entity.ts
1176
+ var import_electrodb3 = require("electrodb");
1177
+ var ConfigurationWorkspaceProjectionEntity = new import_electrodb3.Entity({
1178
+ model: {
1179
+ entity: "configurationWorkspaceProjection",
1180
+ service: "control",
1181
+ version: "01"
1182
+ },
1183
+ attributes: {
1184
+ /**
1185
+ * Tenant the workspace belongs to. Renders as the leading segment
1186
+ * of the base-table PK. Always required — the workspace partition
1187
+ * is tenant-scoped per ADR-011.
1188
+ */
1189
+ tenantId: {
1190
+ type: "string",
1191
+ required: true
1192
+ },
1193
+ /**
1194
+ * Workspace partition discriminator. Renders as the trailing
1195
+ * segment of the base-table PK
1196
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
1197
+ * the projection has no meaning outside a workspace partition.
1198
+ */
1199
+ workspaceId: {
1200
+ type: "string",
1201
+ required: true
1202
+ },
1203
+ /**
1204
+ * Pre-composed sort key — built by the operations-layer projection
1205
+ * writer via `buildConfigurationWorkspaceProjectionSk`. The entity
1206
+ * stores the value verbatim so the SK grammar (pattern #10
1207
+ * workspace-scope) is owned by the operations layer, not
1208
+ * duplicated here.
1209
+ */
1210
+ sk: {
1211
+ type: "string",
1212
+ required: true
1213
+ },
1214
+ /**
1215
+ * Configuration canonical-record id. Stored as a discriminating
1216
+ * field so consumers can hydrate the canonical row via the
1217
+ * Configuration get-by-id operation when the projection's `summary`
1218
+ * is insufficient.
1219
+ */
1220
+ configurationId: {
1221
+ type: "string",
1222
+ required: true
1223
+ },
1224
+ /**
1225
+ * Scope marker. Always `"workspace"` on this projection — recorded
1226
+ * explicitly so future scope-bearing projections (user, tenant,
1227
+ * role) can share filter semantics in a unified cross-projection
1228
+ * list query if one ever lands.
1229
+ */
1230
+ scope: {
1231
+ type: "string",
1232
+ required: true,
1233
+ default: "workspace"
1234
+ },
1235
+ /**
1236
+ * Configuration's `key` attribute (config category, e.g. endpoints,
1237
+ * branding, display). Mirrored from the canonical row so consumers
1238
+ * reading the projection get the natural display label without a
1239
+ * BatchGet hop. Doubles as the source of `<normalizedConfigName>`
1240
+ * in the SK.
1241
+ */
1242
+ displayName: {
1243
+ type: "string",
1244
+ required: false
1245
+ },
1246
+ /**
1247
+ * Summary projection (key display fields as JSON string) — mirrored
1248
+ * from the canonical Configuration row so workspace-partition
1249
+ * queries do not need a BatchGet hop.
1250
+ */
1251
+ summary: {
1252
+ type: "string",
1253
+ required: true
1254
+ },
1255
+ /** Version id mirrored from the canonical Configuration row. */
1256
+ vid: {
1257
+ type: "string",
1258
+ required: true
1259
+ },
1260
+ /** Last-updated timestamp mirrored from the canonical Configuration row. */
1261
+ lastUpdated: {
1262
+ type: "string",
1263
+ required: true
1264
+ }
1265
+ },
1266
+ indexes: {
1267
+ /**
1268
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
1269
+ * SK = operation-supplied. A single
1270
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'CONFIGURATION#')`
1271
+ * returns the workspace's workspace-scoped Configurations sorted by
1272
+ * `<normalizedConfigName>` (then `<configurationId>` as the
1273
+ * tiebreaker).
1274
+ */
1275
+ record: {
1276
+ pk: {
1277
+ field: "PK",
1278
+ composite: ["tenantId", "workspaceId"],
1279
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
1280
+ },
1281
+ sk: {
1282
+ field: "SK",
1283
+ casing: "none",
1284
+ composite: ["sk"],
1285
+ template: "${sk}"
1286
+ }
1287
+ }
1288
+ }
1289
+ });
1290
+
1291
+ // src/data/dynamo/entities/control/membership-entity.ts
1292
+ var import_electrodb4 = require("electrodb");
1293
+ var MembershipEntity = new import_electrodb4.Entity({
935
1294
  model: {
936
1295
  entity: "membership",
937
1296
  service: "control",
@@ -977,8 +1336,14 @@ var MembershipEntity = new import_electrodb2.Entity({
977
1336
  required: true
978
1337
  },
979
1338
  gsi1Shard: gsi1ShardAttribute,
980
- /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
981
- gsi1sk: gsi1skAttribute,
1339
+ /**
1340
+ * Derived GSI1 sort key — `<normalizedUserName>#<id>` per ADR-018
1341
+ * pattern #1 so a GSI1 query partitioned on the tenant range-scans
1342
+ * by user-name prefix and returns memberships sorted by user name.
1343
+ * Falls back to `<lastUpdated>#<id>` when `denormalizedUserName`
1344
+ * is missing.
1345
+ */
1346
+ gsi1sk: membershipGsi1skAttribute,
982
1347
  deleted: {
983
1348
  type: "boolean",
984
1349
  required: false
@@ -1000,6 +1365,36 @@ var MembershipEntity = new import_electrodb2.Entity({
1000
1365
  linkedDataIdentityRef: {
1001
1366
  type: "string",
1002
1367
  required: false
1368
+ },
1369
+ /**
1370
+ * Denormalized display name of the linked Tenant, captured at row
1371
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1372
+ * adjacency-list projection SKs (pattern #3 — `MEMBERSHIP#TENANT#<normalizedTenantName>#…`)
1373
+ * can be composed from a top-level field instead of digging into the
1374
+ * `resource` JSON. Optional on the schema so pre-TR-024 rows do not
1375
+ * break; the operations-layer multi-write helper (#1010) makes the
1376
+ * field load-bearing at write time per TR-024 rule 2 (write-time
1377
+ * source = canonical Tenant.displayName).
1378
+ * @see TR-024 — Denormalized display-name attributes
1379
+ */
1380
+ denormalizedTenantName: {
1381
+ type: "string",
1382
+ required: false
1383
+ },
1384
+ /**
1385
+ * Denormalized display name of the linked User, captured at row
1386
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1387
+ * adjacency-list canonical-record GSI1SK (pattern #1 —
1388
+ * `<normalizedUserName>#<id>`) and workspace-projection SK (pattern #2)
1389
+ * can be composed from a top-level field. Optional on the schema so
1390
+ * pre-TR-024 rows do not break; the operations-layer multi-write helper
1391
+ * (#1010) makes the field load-bearing at write time per TR-024 rule 2
1392
+ * (write-time source = canonical User.displayName).
1393
+ * @see TR-024 — Denormalized display-name attributes
1394
+ */
1395
+ denormalizedUserName: {
1396
+ type: "string",
1397
+ required: false
1003
1398
  }
1004
1399
  },
1005
1400
  indexes: {
@@ -1019,9 +1414,11 @@ var MembershipEntity = new import_electrodb2.Entity({
1019
1414
  /**
1020
1415
  * GSI1 — Unified Sharded List per ADR-011: list all Memberships for a tenant across the
1021
1416
  * four shards. Membership is tenant-scoped only, so `WID#-` is a sentinel.
1022
- * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
1023
- * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
1024
- * normalized label and ISO-8601 `T`/`Z`.
1417
+ * SK is derived via `membershipGsi1skAttribute` — composes
1418
+ * `<normalizedUserName>#<id>` per ADR-018 pattern #1 (users in a
1419
+ * tenant, sorted by user name); falls back to `<lastUpdated>#<id>`
1420
+ * when `denormalizedUserName` is missing. `casing: "none"` preserves
1421
+ * the normalized label and ISO-8601 `T`/`Z`.
1025
1422
  */
1026
1423
  gsi1: {
1027
1424
  index: "GSI1",
@@ -1040,50 +1437,292 @@ var MembershipEntity = new import_electrodb2.Entity({
1040
1437
  }
1041
1438
  });
1042
1439
 
1043
- // src/data/dynamo/entities/control/role-entity.ts
1044
- var import_electrodb3 = require("electrodb");
1045
- var RoleEntity = new import_electrodb3.Entity({
1440
+ // src/data/dynamo/entities/control/membership-user-projection-entity.ts
1441
+ var import_electrodb5 = require("electrodb");
1442
+ var MembershipUserProjectionEntity = new import_electrodb5.Entity({
1046
1443
  model: {
1047
- entity: "role",
1444
+ entity: "membershipUserProjection",
1048
1445
  service: "control",
1049
1446
  version: "01"
1050
1447
  },
1051
1448
  attributes: {
1052
- /** Sort key sentinel. Always "CURRENT". */
1449
+ /**
1450
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
1451
+ * base-table PK. Always required — the projection has no meaning
1452
+ * outside a user partition.
1453
+ */
1454
+ userId: {
1455
+ type: "string",
1456
+ required: true
1457
+ },
1458
+ /**
1459
+ * Pre-composed sort key — built by the operations-layer projection
1460
+ * writer via `buildMembershipUserProjectionSk*` helpers. The entity
1461
+ * stores the value verbatim so the SK grammar (patterns #3 and #4)
1462
+ * is owned by the operations layer, not duplicated here.
1463
+ */
1053
1464
  sk: {
1054
1465
  type: "string",
1055
- required: true,
1056
- default: "CURRENT"
1466
+ required: true
1057
1467
  },
1058
- /** FHIR Resource.id; role id. */
1059
- id: {
1468
+ /** Tenant in which the membership applies. Always required. */
1469
+ tenantId: {
1060
1470
  type: "string",
1061
1471
  required: true
1062
1472
  },
1063
- /** Full Role resource serialized as JSON string. */
1064
- resource: {
1473
+ /**
1474
+ * Workspace the membership scopes to. Present iff the projection
1475
+ * row is a pattern-#4 workspace sub-lane row; absent for pattern-#3
1476
+ * tenant sub-lane rows.
1477
+ */
1478
+ workspaceId: {
1479
+ type: "string",
1480
+ required: false
1481
+ },
1482
+ /**
1483
+ * Membership canonical-record id. Stored as a discriminating field
1484
+ * so consumers can hydrate the canonical row via
1485
+ * `MembershipEntity.get({ tenantId, id: membershipId })` when the
1486
+ * projection's `summary` is insufficient.
1487
+ */
1488
+ membershipId: {
1065
1489
  type: "string",
1066
1490
  required: true
1067
1491
  },
1068
1492
  /**
1069
- * Summary projection (key display fields as JSON string: id, displayName, status).
1070
- * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
1493
+ * Summary projection (key display fields as JSON string: id,
1494
+ * displayName, status) mirrored from the canonical Membership row
1495
+ * so user-partition queries do not need a BatchGet hop.
1071
1496
  */
1072
1497
  summary: {
1073
1498
  type: "string",
1074
1499
  required: true
1075
1500
  },
1076
- /** Version id (e.g. ULID). */
1501
+ /** Version id mirrored from the canonical Membership row. */
1077
1502
  vid: {
1078
1503
  type: "string",
1079
1504
  required: true
1080
1505
  },
1506
+ /** Last-updated timestamp mirrored from the canonical Membership row. */
1081
1507
  lastUpdated: {
1082
1508
  type: "string",
1083
1509
  required: true
1084
1510
  },
1085
- gsi1Shard: gsi1ShardAttribute,
1086
- /** Derived GSI1 sort keyname-based when extractable; else `<lastUpdated>#<id>`. */
1511
+ /**
1512
+ * Denormalized Tenant display namerequired to compose pattern-#3
1513
+ * SK (`MEMBERSHIP#TENANT#<normalizedTenantName>#…`). Optional on the
1514
+ * schema because pre-TR-024 rows may not carry a display name; the
1515
+ * operations layer falls back gracefully when missing.
1516
+ */
1517
+ denormalizedTenantName: {
1518
+ type: "string",
1519
+ required: false
1520
+ },
1521
+ /**
1522
+ * Denormalized User display name — mirrored from the canonical
1523
+ * Membership row per TR-024 rule 3 (canonical-record symmetry).
1524
+ * Carried on the projection so consumers can render the user's
1525
+ * display name without a hop to the User record.
1526
+ */
1527
+ denormalizedUserName: {
1528
+ type: "string",
1529
+ required: false
1530
+ },
1531
+ /**
1532
+ * Denormalized Workspace display name — required to compose
1533
+ * pattern-#4 SK (`MEMBERSHIP#WORKSPACE#TID#<tenantId>#<normalizedWorkspaceName>#…`).
1534
+ * Optional on the schema (TR-024 § Open Item #4 defers a formal
1535
+ * Workspace-rename cascade); the operations layer falls back to a
1536
+ * sentinel when missing so the SK still has a valid shape.
1537
+ */
1538
+ denormalizedWorkspaceName: {
1539
+ type: "string",
1540
+ required: false
1541
+ }
1542
+ },
1543
+ indexes: {
1544
+ /**
1545
+ * Base table: PK = USER#ID#<userId>, SK = operation-supplied.
1546
+ * Both pattern #3 and pattern #4 use this same index — the SK string
1547
+ * encodes the lane discriminator (`MEMBERSHIP#TENANT#…` vs
1548
+ * `MEMBERSHIP#WORKSPACE#…`) so a single `Query(PK = USER#ID#<userId>,
1549
+ * SK begins_with 'MEMBERSHIP#')` returns both lanes interleaved.
1550
+ */
1551
+ record: {
1552
+ pk: {
1553
+ field: "PK",
1554
+ composite: ["userId"],
1555
+ template: "USER#ID#${userId}"
1556
+ },
1557
+ sk: {
1558
+ field: "SK",
1559
+ casing: "none",
1560
+ composite: ["sk"],
1561
+ template: "${sk}"
1562
+ }
1563
+ }
1564
+ }
1565
+ });
1566
+
1567
+ // src/data/dynamo/entities/control/membership-workspace-projection-entity.ts
1568
+ var import_electrodb6 = require("electrodb");
1569
+ var MembershipWorkspaceProjectionEntity = new import_electrodb6.Entity({
1570
+ model: {
1571
+ entity: "membershipWorkspaceProjection",
1572
+ service: "control",
1573
+ version: "01"
1574
+ },
1575
+ attributes: {
1576
+ /**
1577
+ * Tenant the workspace belongs to. Renders as the leading segment
1578
+ * of the base-table PK. Always required — the workspace partition
1579
+ * is tenant-scoped per ADR-011.
1580
+ */
1581
+ tenantId: {
1582
+ type: "string",
1583
+ required: true
1584
+ },
1585
+ /**
1586
+ * Workspace partition discriminator. Renders as the trailing
1587
+ * segment of the base-table PK
1588
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
1589
+ * the projection has no meaning outside a workspace partition.
1590
+ */
1591
+ workspaceId: {
1592
+ type: "string",
1593
+ required: true
1594
+ },
1595
+ /**
1596
+ * Pre-composed sort key — built by the operations-layer projection
1597
+ * writer via `buildMembershipWorkspaceProjectionSk`. The entity
1598
+ * stores the value verbatim so the SK grammar (pattern #2) is
1599
+ * owned by the operations layer, not duplicated here.
1600
+ */
1601
+ sk: {
1602
+ type: "string",
1603
+ required: true
1604
+ },
1605
+ /**
1606
+ * User the membership links. Stored as a discriminating field so
1607
+ * consumers can hydrate the canonical User row via
1608
+ * `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
1609
+ * projection's `summary` is insufficient.
1610
+ */
1611
+ userId: {
1612
+ type: "string",
1613
+ required: true
1614
+ },
1615
+ /**
1616
+ * Membership canonical-record id. Stored as a discriminating field
1617
+ * so consumers can hydrate the canonical row via
1618
+ * `MembershipEntity.get({ tenantId, id: membershipId })` when the
1619
+ * projection's `summary` is insufficient.
1620
+ */
1621
+ membershipId: {
1622
+ type: "string",
1623
+ required: true
1624
+ },
1625
+ /**
1626
+ * Summary projection (key display fields as JSON string: id,
1627
+ * displayName, status) — mirrored from the canonical Membership row
1628
+ * so workspace-partition queries do not need a BatchGet hop.
1629
+ */
1630
+ summary: {
1631
+ type: "string",
1632
+ required: true
1633
+ },
1634
+ /** Version id mirrored from the canonical Membership row. */
1635
+ vid: {
1636
+ type: "string",
1637
+ required: true
1638
+ },
1639
+ /** Last-updated timestamp mirrored from the canonical Membership row. */
1640
+ lastUpdated: {
1641
+ type: "string",
1642
+ required: true
1643
+ },
1644
+ /**
1645
+ * Denormalized User display name — required to compose the
1646
+ * pattern-#2 SK (`MEMBERSHIP#<normalizedUserName>#…`). Optional on
1647
+ * the schema because pre-TR-024 rows may not carry a display name;
1648
+ * the operations layer falls back to a sentinel when missing so
1649
+ * the SK still has a valid shape. The TR-023 rename-cascade
1650
+ * pipeline rewrites the SK on a User rename.
1651
+ */
1652
+ denormalizedUserName: {
1653
+ type: "string",
1654
+ required: false
1655
+ }
1656
+ },
1657
+ indexes: {
1658
+ /**
1659
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
1660
+ * SK = operation-supplied. Pattern #2 uses this index — the SK
1661
+ * encodes the entity-type prefix (`MEMBERSHIP#…`) so a
1662
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'MEMBERSHIP#')`
1663
+ * returns every member projection for the workspace in normalized-
1664
+ * user-name sort order.
1665
+ */
1666
+ record: {
1667
+ pk: {
1668
+ field: "PK",
1669
+ composite: ["tenantId", "workspaceId"],
1670
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
1671
+ },
1672
+ sk: {
1673
+ field: "SK",
1674
+ casing: "none",
1675
+ composite: ["sk"],
1676
+ template: "${sk}"
1677
+ }
1678
+ }
1679
+ }
1680
+ });
1681
+
1682
+ // src/data/dynamo/entities/control/role-entity.ts
1683
+ var import_electrodb7 = require("electrodb");
1684
+ var RoleEntity = new import_electrodb7.Entity({
1685
+ model: {
1686
+ entity: "role",
1687
+ service: "control",
1688
+ version: "01"
1689
+ },
1690
+ attributes: {
1691
+ /** Sort key sentinel. Always "CURRENT". */
1692
+ sk: {
1693
+ type: "string",
1694
+ required: true,
1695
+ default: "CURRENT"
1696
+ },
1697
+ /** FHIR Resource.id; role id. */
1698
+ id: {
1699
+ type: "string",
1700
+ required: true
1701
+ },
1702
+ /** Full Role resource serialized as JSON string. */
1703
+ resource: {
1704
+ type: "string",
1705
+ required: true
1706
+ },
1707
+ /**
1708
+ * Summary projection (key display fields as JSON string: id, displayName, status).
1709
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
1710
+ */
1711
+ summary: {
1712
+ type: "string",
1713
+ required: true
1714
+ },
1715
+ /** Version id (e.g. ULID). */
1716
+ vid: {
1717
+ type: "string",
1718
+ required: true
1719
+ },
1720
+ lastUpdated: {
1721
+ type: "string",
1722
+ required: true
1723
+ },
1724
+ gsi1Shard: gsi1ShardAttribute,
1725
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
1087
1726
  gsi1sk: gsi1skAttribute,
1088
1727
  deleted: {
1089
1728
  type: "boolean",
@@ -1119,127 +1758,460 @@ var RoleEntity = new import_electrodb3.Entity({
1119
1758
  * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
1120
1759
  * normalized label and ISO-8601 `T`/`Z`.
1121
1760
  */
1122
- gsi1: {
1123
- index: "GSI1",
1761
+ gsi1: {
1762
+ index: "GSI1",
1763
+ pk: {
1764
+ field: "GSI1PK",
1765
+ composite: ["gsi1Shard"],
1766
+ template: "TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}"
1767
+ },
1768
+ sk: {
1769
+ field: "GSI1SK",
1770
+ casing: "none",
1771
+ composite: ["gsi1sk"],
1772
+ template: "${gsi1sk}"
1773
+ }
1774
+ }
1775
+ }
1776
+ });
1777
+
1778
+ // src/data/dynamo/entities/control/roleassignment-entity.ts
1779
+ var import_electrodb8 = require("electrodb");
1780
+ var RoleAssignmentEntity = new import_electrodb8.Entity({
1781
+ model: {
1782
+ entity: "roleassignment",
1783
+ service: "control",
1784
+ version: "01"
1785
+ },
1786
+ attributes: {
1787
+ /** Sort key sentinel. Always "CURRENT". */
1788
+ sk: {
1789
+ type: "string",
1790
+ required: true,
1791
+ default: "CURRENT"
1792
+ },
1793
+ /** Tenant in which the role assignment applies (required). */
1794
+ tenantId: {
1795
+ type: "string",
1796
+ required: true
1797
+ },
1798
+ /** FHIR Resource.id; role assignment id. */
1799
+ id: {
1800
+ type: "string",
1801
+ required: true
1802
+ },
1803
+ /** Full RoleAssignment resource serialized as JSON string. */
1804
+ resource: {
1805
+ type: "string",
1806
+ required: true
1807
+ },
1808
+ /**
1809
+ * Summary projection (key display fields as JSON string: id, displayName, status).
1810
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
1811
+ */
1812
+ summary: {
1813
+ type: "string",
1814
+ required: true
1815
+ },
1816
+ /** Version id (e.g. ULID). */
1817
+ vid: {
1818
+ type: "string",
1819
+ required: true
1820
+ },
1821
+ lastUpdated: {
1822
+ type: "string",
1823
+ required: true
1824
+ },
1825
+ gsi1Shard: gsi1ShardAttribute,
1826
+ /**
1827
+ * Derived GSI1 sort key — discriminator-first
1828
+ * `<roleId>#<normalizedUserName>#<id>` per ADR-018 pattern #8 so a
1829
+ * GSI1 query partitioned on the tenant can `begins_with('<roleId>#')`
1830
+ * to enumerate every user assigned to a given role, sorted by user
1831
+ * name. Falls back to `<lastUpdated>#<id>` when either component is
1832
+ * missing.
1833
+ */
1834
+ gsi1sk: roleAssignmentGsi1skAttribute,
1835
+ deleted: {
1836
+ type: "boolean",
1837
+ required: false
1838
+ },
1839
+ bundleId: {
1840
+ type: "string",
1841
+ required: false
1842
+ },
1843
+ msgId: {
1844
+ type: "string",
1845
+ required: false
1846
+ },
1847
+ /**
1848
+ * Denormalized display name of the linked Tenant, captured at row
1849
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1850
+ * adjacency-list user-projection SK (pattern #5 —
1851
+ * `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#<roleId>#TID#<tenantId>#<id>`)
1852
+ * can be composed from a top-level field instead of digging into the
1853
+ * `resource` JSON. Optional on the schema so pre-TR-024 rows do not
1854
+ * break; the operations-layer multi-write helper (#1010) makes the
1855
+ * field load-bearing at write time per TR-024 rule 2 (write-time
1856
+ * source = canonical Tenant.displayName).
1857
+ * @see TR-024 — Denormalized display-name attributes
1858
+ */
1859
+ denormalizedTenantName: {
1860
+ type: "string",
1861
+ required: false
1862
+ },
1863
+ /**
1864
+ * Denormalized display name of the linked User, captured at row
1865
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1866
+ * adjacency-list canonical-record GSI1SK (pattern #8 —
1867
+ * `<roleId>#<normalizedUserName>#<id>`) and workspace-projection SK
1868
+ * (pattern #9) can be composed from a top-level field. Optional on
1869
+ * the schema so pre-TR-024 rows do not break; the operations-layer
1870
+ * multi-write helper (#1010) makes the field load-bearing at write
1871
+ * time per TR-024 rule 2 (write-time source = canonical
1872
+ * User.displayName).
1873
+ * @see TR-024 — Denormalized display-name attributes
1874
+ */
1875
+ denormalizedUserName: {
1876
+ type: "string",
1877
+ required: false
1878
+ },
1879
+ /**
1880
+ * Denormalized display name of the linked Role, captured at row
1881
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1882
+ * adjacency-list user-projection SK (pattern #5 —
1883
+ * `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#…`) can be composed from
1884
+ * a top-level field. Optional on the schema so pre-TR-024 rows do not
1885
+ * break; the operations-layer multi-write helper (#1010) makes the
1886
+ * field load-bearing at write time per TR-024 rule 2 (write-time
1887
+ * source = canonical Role.displayName).
1888
+ * @see TR-024 — Denormalized display-name attributes
1889
+ */
1890
+ denormalizedRoleName: {
1891
+ type: "string",
1892
+ required: false
1893
+ }
1894
+ },
1895
+ indexes: {
1896
+ /** Base table: PK = TID#<tenantId>#ROLEASSIGNMENT#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
1897
+ record: {
1898
+ pk: {
1899
+ field: "PK",
1900
+ composite: ["tenantId", "id"],
1901
+ template: "TID#${tenantId}#ROLEASSIGNMENT#ID#${id}"
1902
+ },
1903
+ sk: {
1904
+ field: "SK",
1905
+ composite: ["sk"],
1906
+ template: "${sk}"
1907
+ }
1908
+ },
1909
+ /**
1910
+ * GSI1 — Unified Sharded List per ADR-011: list all RoleAssignments for a tenant across the
1911
+ * four shards. Tenant-scoped only, so `WID#-` is a sentinel.
1912
+ * SK is derived via `roleAssignmentGsi1skAttribute` — composes the
1913
+ * discriminator-first `<roleId>#<normalizedUserName>#<id>` shape per
1914
+ * ADR-018 pattern #8 (users with a specific role in a tenant, sorted
1915
+ * by user name); falls back to `<lastUpdated>#<id>` when either
1916
+ * component is missing. `casing: "none"` preserves the normalized
1917
+ * label and ISO-8601 `T`/`Z`.
1918
+ */
1919
+ gsi1: {
1920
+ index: "GSI1",
1921
+ pk: {
1922
+ field: "GSI1PK",
1923
+ composite: ["tenantId", "gsi1Shard"],
1924
+ template: "TID#${tenantId}#WID#-#RT#RoleAssignment#SHARD#${gsi1Shard}"
1925
+ },
1926
+ sk: {
1927
+ field: "GSI1SK",
1928
+ casing: "none",
1929
+ composite: ["gsi1sk"],
1930
+ template: "${gsi1sk}"
1931
+ }
1932
+ }
1933
+ }
1934
+ });
1935
+
1936
+ // src/data/dynamo/entities/control/roleassignment-user-projection-entity.ts
1937
+ var import_electrodb9 = require("electrodb");
1938
+ var RoleAssignmentUserProjectionEntity = new import_electrodb9.Entity({
1939
+ model: {
1940
+ entity: "roleAssignmentUserProjection",
1941
+ service: "control",
1942
+ version: "01"
1943
+ },
1944
+ attributes: {
1945
+ /**
1946
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
1947
+ * base-table PK. Always required — the projection has no meaning
1948
+ * outside a user partition.
1949
+ */
1950
+ userId: {
1951
+ type: "string",
1952
+ required: true
1953
+ },
1954
+ /**
1955
+ * Pre-composed sort key — built by the operations-layer projection
1956
+ * writer via `buildRoleAssignmentUserProjectionSk*` helpers. The
1957
+ * entity stores the value verbatim so the SK grammar (tenant-lane
1958
+ * vs workspace-lane) is owned by the operations layer, not
1959
+ * duplicated here.
1960
+ */
1961
+ sk: {
1962
+ type: "string",
1963
+ required: true
1964
+ },
1965
+ /** Tenant in which the role assignment applies. Always required. */
1966
+ tenantId: {
1967
+ type: "string",
1968
+ required: true
1969
+ },
1970
+ /**
1971
+ * Workspace the role assignment scopes to. Present iff the
1972
+ * projection row is the workspace-level sub-lane; absent for
1973
+ * tenant-level sub-lane rows.
1974
+ */
1975
+ workspaceId: {
1976
+ type: "string",
1977
+ required: false
1978
+ },
1979
+ /**
1980
+ * Role the assignment grants. Stored as a discriminating field so
1981
+ * `Query(PK = USER#ID#<userId>, SK begins_with 'ROLEASSIGNMENT#…')`
1982
+ * results carry the role id without a hop to the canonical row.
1983
+ */
1984
+ roleId: {
1985
+ type: "string",
1986
+ required: true
1987
+ },
1988
+ /**
1989
+ * RoleAssignment canonical-record id. Stored as a discriminating
1990
+ * field so consumers can hydrate the canonical row via
1991
+ * `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
1992
+ * when the projection's `summary` is insufficient.
1993
+ */
1994
+ roleAssignmentId: {
1995
+ type: "string",
1996
+ required: true
1997
+ },
1998
+ /**
1999
+ * Summary projection (key display fields as JSON string: id,
2000
+ * displayName, status) — mirrored from the canonical RoleAssignment
2001
+ * row so user-partition queries do not need a BatchGet hop.
2002
+ */
2003
+ summary: {
2004
+ type: "string",
2005
+ required: true
2006
+ },
2007
+ /** Version id mirrored from the canonical RoleAssignment row. */
2008
+ vid: {
2009
+ type: "string",
2010
+ required: true
2011
+ },
2012
+ /** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
2013
+ lastUpdated: {
2014
+ type: "string",
2015
+ required: true
2016
+ },
2017
+ /**
2018
+ * Denormalized Tenant display name — mirrored from the canonical
2019
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
2020
+ * Optional on the schema because pre-TR-024 rows may not carry a
2021
+ * display name; the operations layer falls back gracefully when
2022
+ * missing.
2023
+ */
2024
+ denormalizedTenantName: {
2025
+ type: "string",
2026
+ required: false
2027
+ },
2028
+ /**
2029
+ * Denormalized User display name — mirrored from the canonical
2030
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
2031
+ * Carried on the projection so consumers can render the user's
2032
+ * display name without a hop to the User record.
2033
+ */
2034
+ denormalizedUserName: {
2035
+ type: "string",
2036
+ required: false
2037
+ },
2038
+ /**
2039
+ * Denormalized Role display name — required to compose the SK's
2040
+ * `<normalizedRoleName>` segment. Optional on the schema (pre-TR-024
2041
+ * rows fall back to a sentinel) but expected to be present at write
2042
+ * time per TR-024 rule 2 (write-time source =
2043
+ * canonical Role.displayName).
2044
+ */
2045
+ denormalizedRoleName: {
2046
+ type: "string",
2047
+ required: false
2048
+ }
2049
+ },
2050
+ indexes: {
2051
+ /**
2052
+ * Base table: PK = USER#ID#<userId>, SK = operation-supplied. Both
2053
+ * sub-lanes (tenant-level and workspace-level) use this same index —
2054
+ * the SK string encodes the lane discriminator
2055
+ * (`ROLEASSIGNMENT#TENANT#…` vs `ROLEASSIGNMENT#WORKSPACE#…`) so a
2056
+ * single `Query(PK = USER#ID#<userId>, SK begins_with
2057
+ * 'ROLEASSIGNMENT#')` returns both lanes interleaved.
2058
+ */
2059
+ record: {
1124
2060
  pk: {
1125
- field: "GSI1PK",
1126
- composite: ["gsi1Shard"],
1127
- template: "TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}"
2061
+ field: "PK",
2062
+ composite: ["userId"],
2063
+ template: "USER#ID#${userId}"
1128
2064
  },
1129
2065
  sk: {
1130
- field: "GSI1SK",
2066
+ field: "SK",
1131
2067
  casing: "none",
1132
- composite: ["gsi1sk"],
1133
- template: "${gsi1sk}"
2068
+ composite: ["sk"],
2069
+ template: "${sk}"
1134
2070
  }
1135
2071
  }
1136
2072
  }
1137
2073
  });
1138
2074
 
1139
- // src/data/dynamo/entities/control/roleassignment-entity.ts
1140
- var import_electrodb4 = require("electrodb");
1141
- var RoleAssignmentEntity = new import_electrodb4.Entity({
2075
+ // src/data/dynamo/entities/control/roleassignment-workspace-projection-entity.ts
2076
+ var import_electrodb10 = require("electrodb");
2077
+ var RoleAssignmentWorkspaceProjectionEntity = new import_electrodb10.Entity({
1142
2078
  model: {
1143
- entity: "roleassignment",
2079
+ entity: "roleAssignmentWorkspaceProjection",
1144
2080
  service: "control",
1145
2081
  version: "01"
1146
2082
  },
1147
2083
  attributes: {
1148
- /** Sort key sentinel. Always "CURRENT". */
2084
+ /**
2085
+ * Tenant the workspace belongs to. Renders as the leading segment
2086
+ * of the base-table PK. Always required — the workspace partition
2087
+ * is tenant-scoped per ADR-011.
2088
+ */
2089
+ tenantId: {
2090
+ type: "string",
2091
+ required: true
2092
+ },
2093
+ /**
2094
+ * Workspace partition discriminator. Renders as the trailing
2095
+ * segment of the base-table PK
2096
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
2097
+ * the projection has no meaning outside a workspace partition.
2098
+ */
2099
+ workspaceId: {
2100
+ type: "string",
2101
+ required: true
2102
+ },
2103
+ /**
2104
+ * Pre-composed sort key — built by the operations-layer projection
2105
+ * writer via `buildRoleAssignmentWorkspaceProjectionSk`. The entity
2106
+ * stores the value verbatim so the SK grammar (pattern #9) is
2107
+ * owned by the operations layer, not duplicated here.
2108
+ */
1149
2109
  sk: {
1150
2110
  type: "string",
1151
- required: true,
1152
- default: "CURRENT"
2111
+ required: true
1153
2112
  },
1154
- /** Tenant in which the role assignment applies (required). */
1155
- tenantId: {
2113
+ /**
2114
+ * User the role assignment grants the role to. Stored as a
2115
+ * discriminating field so consumers can hydrate the canonical User
2116
+ * row via `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
2117
+ * projection's `summary` is insufficient.
2118
+ */
2119
+ userId: {
1156
2120
  type: "string",
1157
2121
  required: true
1158
2122
  },
1159
- /** FHIR Resource.id; role assignment id. */
1160
- id: {
2123
+ /**
2124
+ * Role the assignment grants. Stored as a discriminating field —
2125
+ * also rendered into the SK as the discriminator-first segment so
2126
+ * `begins_with('ROLEASSIGNMENT#<roleId>#')` filters one role.
2127
+ */
2128
+ roleId: {
1161
2129
  type: "string",
1162
2130
  required: true
1163
2131
  },
1164
- /** Full RoleAssignment resource serialized as JSON string. */
1165
- resource: {
2132
+ /**
2133
+ * RoleAssignment canonical-record id. Stored as a discriminating
2134
+ * field so consumers can hydrate the canonical row via
2135
+ * `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
2136
+ * when the projection's `summary` is insufficient.
2137
+ */
2138
+ roleAssignmentId: {
1166
2139
  type: "string",
1167
2140
  required: true
1168
2141
  },
1169
2142
  /**
1170
- * Summary projection (key display fields as JSON string: id, displayName, status).
1171
- * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
2143
+ * Summary projection (key display fields as JSON string: id,
2144
+ * displayName, status) mirrored from the canonical RoleAssignment
2145
+ * row so workspace-partition queries do not need a BatchGet hop.
1172
2146
  */
1173
2147
  summary: {
1174
2148
  type: "string",
1175
2149
  required: true
1176
2150
  },
1177
- /** Version id (e.g. ULID). */
2151
+ /** Version id mirrored from the canonical RoleAssignment row. */
1178
2152
  vid: {
1179
2153
  type: "string",
1180
2154
  required: true
1181
2155
  },
2156
+ /** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
1182
2157
  lastUpdated: {
1183
2158
  type: "string",
1184
2159
  required: true
1185
2160
  },
1186
- gsi1Shard: gsi1ShardAttribute,
1187
- /** Derived GSI1 sort keyname-based when extractable; else `<lastUpdated>#<id>`. */
1188
- gsi1sk: gsi1skAttribute,
1189
- deleted: {
1190
- type: "boolean",
1191
- required: false
1192
- },
1193
- bundleId: {
2161
+ /**
2162
+ * Denormalized User display namerequired to compose the
2163
+ * pattern-#9 SK (`ROLEASSIGNMENT#<roleId>#<normalizedUserName>#…`).
2164
+ * Optional on the schema because pre-TR-024 rows may not carry a
2165
+ * display name; the operations layer falls back to a sentinel when
2166
+ * missing so the SK still has a valid shape. The TR-023 rename-
2167
+ * cascade pipeline rewrites the SK on a User rename.
2168
+ */
2169
+ denormalizedUserName: {
1194
2170
  type: "string",
1195
2171
  required: false
1196
2172
  },
1197
- msgId: {
2173
+ /**
2174
+ * Denormalized Role display name — mirrored from the canonical
2175
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
2176
+ * Carried on the projection so consumers can render the role's
2177
+ * display name without a hop to the Role record. Not part of the
2178
+ * SK (pattern #9 sorts on `<normalizedUserName>`, not role name) —
2179
+ * a Role rename does NOT rewrite this SK.
2180
+ */
2181
+ denormalizedRoleName: {
1198
2182
  type: "string",
1199
2183
  required: false
1200
2184
  }
1201
2185
  },
1202
2186
  indexes: {
1203
- /** Base table: PK = TID#<tenantId>#ROLEASSIGNMENT#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
2187
+ /**
2188
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
2189
+ * SK = operation-supplied. Pattern #9 uses this index — the SK
2190
+ * encodes the entity-type prefix and discriminator-first roleId
2191
+ * (`ROLEASSIGNMENT#<roleId>#…`) so
2192
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'ROLEASSIGNMENT#<roleId>#')`
2193
+ * returns every user-assignment for that role in the workspace, sorted
2194
+ * by normalized user name.
2195
+ */
1204
2196
  record: {
1205
2197
  pk: {
1206
2198
  field: "PK",
1207
- composite: ["tenantId", "id"],
1208
- template: "TID#${tenantId}#ROLEASSIGNMENT#ID#${id}"
2199
+ composite: ["tenantId", "workspaceId"],
2200
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
1209
2201
  },
1210
2202
  sk: {
1211
2203
  field: "SK",
2204
+ casing: "none",
1212
2205
  composite: ["sk"],
1213
2206
  template: "${sk}"
1214
2207
  }
1215
- },
1216
- /**
1217
- * GSI1 — Unified Sharded List per ADR-011: list all RoleAssignments for a tenant across the
1218
- * four shards. Tenant-scoped only, so `WID#-` is a sentinel.
1219
- * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
1220
- * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
1221
- * normalized label and ISO-8601 `T`/`Z`.
1222
- */
1223
- gsi1: {
1224
- index: "GSI1",
1225
- pk: {
1226
- field: "GSI1PK",
1227
- composite: ["tenantId", "gsi1Shard"],
1228
- template: "TID#${tenantId}#WID#-#RT#RoleAssignment#SHARD#${gsi1Shard}"
1229
- },
1230
- sk: {
1231
- field: "GSI1SK",
1232
- casing: "none",
1233
- composite: ["gsi1sk"],
1234
- template: "${gsi1sk}"
1235
- }
1236
2208
  }
1237
2209
  }
1238
2210
  });
1239
2211
 
1240
2212
  // src/data/dynamo/entities/control/tenant-entity.ts
1241
- var import_electrodb5 = require("electrodb");
1242
- var TenantEntity = new import_electrodb5.Entity({
2213
+ var import_electrodb11 = require("electrodb");
2214
+ var TenantEntity = new import_electrodb11.Entity({
1243
2215
  model: {
1244
2216
  entity: "tenant",
1245
2217
  service: "control",
@@ -1339,8 +2311,8 @@ var TenantEntity = new import_electrodb5.Entity({
1339
2311
  });
1340
2312
 
1341
2313
  // src/data/dynamo/entities/control/user-entity.ts
1342
- var import_electrodb6 = require("electrodb");
1343
- var UserEntity = new import_electrodb6.Entity({
2314
+ var import_electrodb12 = require("electrodb");
2315
+ var UserEntity = new import_electrodb12.Entity({
1344
2316
  model: {
1345
2317
  entity: "user",
1346
2318
  service: "control",
@@ -1395,6 +2367,28 @@ var UserEntity = new import_electrodb6.Entity({
1395
2367
  type: "boolean",
1396
2368
  required: false
1397
2369
  },
2370
+ /**
2371
+ * TR-022 / ADR-018 lifecycle state for the cascade pipeline.
2372
+ *
2373
+ * - `active` (or undefined) — normal, readable state.
2374
+ * - `deleting` — intermediate state set synchronously by the
2375
+ * hard-delete API entry point. The owning-delete cascade state
2376
+ * machine fans out from this transition (DynamoDB stream →
2377
+ * `control-plane.owning-delete.v1` → Step Functions). Readers MUST
2378
+ * short-circuit on `deleting` so partial cascades stay invisible.
2379
+ * - `deleted-failed` — terminal failure state set by the cascade
2380
+ * finalize Lambda when the cascade run fails irrecoverably.
2381
+ * Operators recover by re-running the cascade or by direct
2382
+ * intervention.
2383
+ *
2384
+ * The cascade finalize step deletes the canonical record conditional
2385
+ * on `lifecycleState = "deleting"`; on replay the conditional check
2386
+ * fails and the finalize step treats that as a no-op success.
2387
+ */
2388
+ lifecycleState: {
2389
+ type: ["active", "deleting", "deleted-failed"],
2390
+ required: false
2391
+ },
1398
2392
  bundleId: {
1399
2393
  type: "string",
1400
2394
  required: false
@@ -1464,8 +2458,8 @@ var UserEntity = new import_electrodb6.Entity({
1464
2458
  });
1465
2459
 
1466
2460
  // src/data/dynamo/entities/control/workspace-entity.ts
1467
- var import_electrodb7 = require("electrodb");
1468
- var WorkspaceEntity = new import_electrodb7.Entity({
2461
+ var import_electrodb13 = require("electrodb");
2462
+ var WorkspaceEntity = new import_electrodb13.Entity({
1469
2463
  model: {
1470
2464
  entity: "workspace",
1471
2465
  service: "control",
@@ -1517,6 +2511,28 @@ var WorkspaceEntity = new import_electrodb7.Entity({
1517
2511
  type: "boolean",
1518
2512
  required: false
1519
2513
  },
2514
+ /**
2515
+ * TR-022 / ADR-018 lifecycle state for the cascade pipeline.
2516
+ *
2517
+ * - `active` (or undefined) — normal, readable state.
2518
+ * - `deleting` — intermediate state set synchronously by the
2519
+ * hard-delete API entry point. The owning-delete cascade state
2520
+ * machine fans out from this transition (DynamoDB stream →
2521
+ * `control-plane.owning-delete.v1` → Step Functions). Readers MUST
2522
+ * short-circuit on `deleting` so partial cascades stay invisible.
2523
+ * - `deleted-failed` — terminal failure state set by the cascade
2524
+ * finalize Lambda when the cascade run fails irrecoverably.
2525
+ * Operators recover by re-running the cascade or by direct
2526
+ * intervention.
2527
+ *
2528
+ * The cascade finalize step deletes the canonical record conditional
2529
+ * on `lifecycleState = "deleting"`; on replay the conditional check
2530
+ * fails and the finalize step treats that as a no-op success.
2531
+ */
2532
+ lifecycleState: {
2533
+ type: ["active", "deleting", "deleted-failed"],
2534
+ required: false
2535
+ },
1520
2536
  bundleId: {
1521
2537
  type: "string",
1522
2538
  required: false
@@ -1567,28 +2583,36 @@ var WorkspaceEntity = new import_electrodb7.Entity({
1567
2583
  // src/data/dynamo/dynamo-control-service.ts
1568
2584
  var controlPlaneEntities = {
1569
2585
  configuration: ConfigurationEntity,
2586
+ configurationUserProjection: ConfigurationUserProjectionEntity,
2587
+ configurationWorkspaceProjection: ConfigurationWorkspaceProjectionEntity,
1570
2588
  membership: MembershipEntity,
2589
+ membershipUserProjection: MembershipUserProjectionEntity,
2590
+ membershipWorkspaceProjection: MembershipWorkspaceProjectionEntity,
1571
2591
  role: RoleEntity,
1572
2592
  roleAssignment: RoleAssignmentEntity,
2593
+ roleAssignmentUserProjection: RoleAssignmentUserProjectionEntity,
2594
+ roleAssignmentWorkspaceProjection: RoleAssignmentWorkspaceProjectionEntity,
1573
2595
  tenant: TenantEntity,
1574
2596
  user: UserEntity,
1575
2597
  workspace: WorkspaceEntity
1576
2598
  };
1577
- var controlPlaneService = new import_electrodb8.Service(controlPlaneEntities, {
2599
+ var controlPlaneService = new import_electrodb14.Service(controlPlaneEntities, {
1578
2600
  table: defaultTableName,
1579
2601
  client: dynamoClient
1580
2602
  });
1581
2603
  var DynamoControlService = {
1582
- entities: controlPlaneService.entities
2604
+ entities: controlPlaneService.entities,
2605
+ transaction: controlPlaneService.transaction
1583
2606
  };
1584
2607
  function getDynamoControlService(tableName) {
1585
2608
  const resolved = tableName ?? defaultTableName;
1586
- const service = new import_electrodb8.Service(controlPlaneEntities, {
2609
+ const service = new import_electrodb14.Service(controlPlaneEntities, {
1587
2610
  table: resolved,
1588
2611
  client: dynamoClient
1589
2612
  });
1590
2613
  return {
1591
- entities: service.entities
2614
+ entities: service.entities,
2615
+ transaction: service.transaction
1592
2616
  };
1593
2617
  }
1594
2618
 
@@ -1612,9 +2636,222 @@ var ValidationError = class extends DomainError {
1612
2636
  super(message, "VALIDATION", options);
1613
2637
  }
1614
2638
  };
2639
+ var ConflictError = class extends DomainError {
2640
+ constructor(message, options) {
2641
+ super(message, "CONFLICT", options);
2642
+ }
2643
+ };
1615
2644
 
1616
2645
  // src/data/operations/control/membership/membership-create-operation.ts
2646
+ var import_types5 = require("@openhi/types");
2647
+
2648
+ // src/data/operations/control/membership/membership-user-projection.ts
1617
2649
  var import_types3 = require("@openhi/types");
2650
+ var MISSING_NAME_SENTINEL = "-";
2651
+ function buildMembershipUserProjectionSkTenantLane(params) {
2652
+ const normalizedTenantName = typeof params.denormalizedTenantName === "string" && params.denormalizedTenantName.length > 0 ? (0, import_types3.normalizeLabel)(params.denormalizedTenantName) : MISSING_NAME_SENTINEL;
2653
+ return `MEMBERSHIP#TENANT#${normalizedTenantName}#TID#${params.tenantId}#${params.membershipId}`;
2654
+ }
2655
+ function buildMembershipUserProjectionSkWorkspaceLane(params) {
2656
+ const normalizedWorkspaceName = typeof params.denormalizedWorkspaceName === "string" && params.denormalizedWorkspaceName.length > 0 ? (0, import_types3.normalizeLabel)(params.denormalizedWorkspaceName) : MISSING_NAME_SENTINEL;
2657
+ return `MEMBERSHIP#WORKSPACE#TID#${params.tenantId}#${normalizedWorkspaceName}#WID#${params.workspaceId}#${params.membershipId}`;
2658
+ }
2659
+ function buildMembershipUserProjectionItem(input) {
2660
+ if (!input.userId || input.userId.length === 0) {
2661
+ return void 0;
2662
+ }
2663
+ const hasWorkspace = typeof input.workspaceId === "string" && input.workspaceId.length > 0;
2664
+ const sk = hasWorkspace ? buildMembershipUserProjectionSkWorkspaceLane({
2665
+ tenantId: input.tenantId,
2666
+ workspaceId: input.workspaceId,
2667
+ membershipId: input.membershipId,
2668
+ denormalizedWorkspaceName: input.denormalizedWorkspaceName
2669
+ }) : buildMembershipUserProjectionSkTenantLane({
2670
+ tenantId: input.tenantId,
2671
+ membershipId: input.membershipId,
2672
+ denormalizedTenantName: input.denormalizedTenantName
2673
+ });
2674
+ return {
2675
+ userId: input.userId,
2676
+ sk,
2677
+ tenantId: input.tenantId,
2678
+ workspaceId: hasWorkspace ? input.workspaceId : void 0,
2679
+ membershipId: input.membershipId,
2680
+ summary: input.summary,
2681
+ vid: input.vid,
2682
+ lastUpdated: input.lastUpdated,
2683
+ denormalizedTenantName: input.denormalizedTenantName,
2684
+ denormalizedUserName: input.denormalizedUserName,
2685
+ denormalizedWorkspaceName: hasWorkspace ? input.denormalizedWorkspaceName : void 0
2686
+ };
2687
+ }
2688
+ function extractReferenceSlug(resource, fieldName) {
2689
+ const field = resource[fieldName];
2690
+ if (!field || typeof field !== "object") {
2691
+ return void 0;
2692
+ }
2693
+ const reference = field.reference;
2694
+ if (typeof reference !== "string" || reference.length === 0) {
2695
+ return void 0;
2696
+ }
2697
+ const slash = reference.lastIndexOf("/");
2698
+ const tail = slash >= 0 ? reference.slice(slash + 1) : reference;
2699
+ return tail.length > 0 ? tail : void 0;
2700
+ }
2701
+
2702
+ // src/data/operations/control/membership/membership-workspace-projection.ts
2703
+ var import_types4 = require("@openhi/types");
2704
+ var MISSING_NAME_SENTINEL2 = "-";
2705
+ function buildMembershipWorkspaceProjectionSk(params) {
2706
+ const normalizedUserName = typeof params.denormalizedUserName === "string" && params.denormalizedUserName.length > 0 ? (0, import_types4.normalizeLabel)(params.denormalizedUserName) : MISSING_NAME_SENTINEL2;
2707
+ return `MEMBERSHIP#${normalizedUserName}#USER#${params.userId}#${params.membershipId}`;
2708
+ }
2709
+ function buildMembershipWorkspaceProjectionItem(input) {
2710
+ if (!input.workspaceId || input.workspaceId.length === 0) {
2711
+ return void 0;
2712
+ }
2713
+ if (!input.userId || input.userId.length === 0) {
2714
+ return void 0;
2715
+ }
2716
+ const sk = buildMembershipWorkspaceProjectionSk({
2717
+ userId: input.userId,
2718
+ membershipId: input.membershipId,
2719
+ denormalizedUserName: input.denormalizedUserName
2720
+ });
2721
+ return {
2722
+ tenantId: input.tenantId,
2723
+ workspaceId: input.workspaceId,
2724
+ sk,
2725
+ userId: input.userId,
2726
+ membershipId: input.membershipId,
2727
+ summary: input.summary,
2728
+ vid: input.vid,
2729
+ lastUpdated: input.lastUpdated,
2730
+ denormalizedUserName: input.denormalizedUserName
2731
+ };
2732
+ }
2733
+
2734
+ // src/data/operations/control/denormalized-display-names.ts
2735
+ function extractDenormalizedReferenceDisplay(resource, fieldName) {
2736
+ const field = resource[fieldName];
2737
+ if (!field || typeof field !== "object") {
2738
+ return void 0;
2739
+ }
2740
+ const display = field.display;
2741
+ if (typeof display !== "string") {
2742
+ return void 0;
2743
+ }
2744
+ const trimmed = display.trim();
2745
+ return trimmed.length > 0 ? trimmed : void 0;
2746
+ }
2747
+
2748
+ // src/data/operations/control/multi-write-operation.ts
2749
+ var TRANSACT_WRITE_ITEM_LIMIT = 100;
2750
+ async function executeMultiWrite(params) {
2751
+ const { service, triples, token } = params;
2752
+ if (triples.length === 0) {
2753
+ throw new ValidationError(
2754
+ "executeMultiWrite called with zero triples; at least one triple is required"
2755
+ );
2756
+ }
2757
+ if (triples.length > TRANSACT_WRITE_ITEM_LIMIT) {
2758
+ throw new ValidationError(
2759
+ `executeMultiWrite received ${triples.length} triples; DynamoDB TransactWriteItems is limited to ${TRANSACT_WRITE_ITEM_LIMIT} items per call`,
2760
+ {
2761
+ details: {
2762
+ itemsRequested: triples.length,
2763
+ limit: TRANSACT_WRITE_ITEM_LIMIT
2764
+ }
2765
+ }
2766
+ );
2767
+ }
2768
+ for (const [index, triple] of triples.entries()) {
2769
+ if (!triple || typeof triple !== "object") {
2770
+ throw new ValidationError(
2771
+ `executeMultiWrite triple at index ${index} is not an object`
2772
+ );
2773
+ }
2774
+ if (typeof triple.entity !== "string" || triple.entity.length === 0) {
2775
+ throw new ValidationError(
2776
+ `executeMultiWrite triple at index ${index} is missing a non-empty 'entity' key`
2777
+ );
2778
+ }
2779
+ if (!isSupportedAction(triple.action)) {
2780
+ throw new ValidationError(
2781
+ `executeMultiWrite triple at index ${index} has unsupported action '${String(
2782
+ triple.action
2783
+ )}'; supported: 'put' | 'create' | 'delete'`
2784
+ );
2785
+ }
2786
+ if (!triple.item || typeof triple.item !== "object") {
2787
+ throw new ValidationError(
2788
+ `executeMultiWrite triple at index ${index} is missing an 'item' payload`
2789
+ );
2790
+ }
2791
+ }
2792
+ let result;
2793
+ try {
2794
+ result = await service.transaction.write(
2795
+ (entities) => triples.map((triple, index) => {
2796
+ const transactEntity = entities[triple.entity];
2797
+ if (transactEntity === void 0) {
2798
+ throw new ValidationError(
2799
+ `executeMultiWrite triple at index ${index} references unknown entity '${triple.entity}'; ensure the service exposes it`
2800
+ );
2801
+ }
2802
+ switch (triple.action) {
2803
+ case "put":
2804
+ return transactEntity.put(triple.item).commit();
2805
+ case "create":
2806
+ return transactEntity.create(triple.item).commit();
2807
+ case "delete":
2808
+ return transactEntity.delete(triple.item).commit();
2809
+ default:
2810
+ throw new ValidationError(
2811
+ `executeMultiWrite triple at index ${index} has unsupported action '${String(
2812
+ triple.action
2813
+ )}'`
2814
+ );
2815
+ }
2816
+ })
2817
+ ).go(token === void 0 ? void 0 : { token });
2818
+ } catch (err) {
2819
+ if (err instanceof DomainError) {
2820
+ throw err;
2821
+ }
2822
+ throw new ConflictError(buildCancellationMessage(err), {
2823
+ cause: err,
2824
+ details: extractCancellationReasons(err)
2825
+ });
2826
+ }
2827
+ if (result.canceled) {
2828
+ throw new ConflictError(
2829
+ "TransactWriteItems was canceled by DynamoDB (check CancellationReasons on the cause for details)",
2830
+ { details: { canceled: true, data: result.data } }
2831
+ );
2832
+ }
2833
+ return { itemsWritten: triples.length, canceled: false };
2834
+ }
2835
+ function isSupportedAction(value) {
2836
+ return value === "put" || value === "create" || value === "delete";
2837
+ }
2838
+ function buildCancellationMessage(err) {
2839
+ if (err instanceof Error && err.message) {
2840
+ return `TransactWriteItems failed: ${err.message}`;
2841
+ }
2842
+ return "TransactWriteItems failed (no error message available)";
2843
+ }
2844
+ function extractCancellationReasons(err) {
2845
+ if (err && typeof err === "object") {
2846
+ const cancellationReasons = err.CancellationReasons;
2847
+ if (cancellationReasons !== void 0) {
2848
+ return { CancellationReasons: cancellationReasons };
2849
+ }
2850
+ }
2851
+ return void 0;
2852
+ }
2853
+
2854
+ // src/data/operations/control/membership/membership-create-operation.ts
1618
2855
  async function createMembershipOperation(params) {
1619
2856
  const { context, body, tableName } = params;
1620
2857
  const service = getDynamoControlService(tableName);
@@ -1625,26 +2862,86 @@ async function createMembershipOperation(params) {
1625
2862
  const resource = { resourceType: "Membership", id, ...parsedResource };
1626
2863
  let linkedDataIdentityRef;
1627
2864
  try {
1628
- const ext = (0, import_types3.assertLinkedDataIdentityCardinality)(
2865
+ const ext = (0, import_types5.assertLinkedDataIdentityCardinality)(
1629
2866
  resource
1630
2867
  );
1631
2868
  linkedDataIdentityRef = ext?.valueReference?.reference;
1632
2869
  } catch (e) {
1633
- if (e instanceof import_types3.LinkedDataIdentityCardinalityError) {
2870
+ if (e instanceof import_types5.LinkedDataIdentityCardinalityError) {
1634
2871
  throw new ValidationError(e.message, { cause: e });
1635
2872
  }
1636
2873
  throw e;
1637
2874
  }
1638
- const summary = JSON.stringify((0, import_types3.extractSummary)(resource));
1639
- await service.entities.membership.put({
2875
+ const resourceRecord = resource;
2876
+ const denormalizedTenantName = extractDenormalizedReferenceDisplay(
2877
+ resourceRecord,
2878
+ "tenant"
2879
+ );
2880
+ const denormalizedUserName = extractDenormalizedReferenceDisplay(
2881
+ resourceRecord,
2882
+ "user"
2883
+ );
2884
+ const denormalizedWorkspaceName = extractDenormalizedReferenceDisplay(
2885
+ resourceRecord,
2886
+ "workspace"
2887
+ );
2888
+ const summary = JSON.stringify((0, import_types5.extractSummary)(resource));
2889
+ const userIdFromResource = extractReferenceSlug(resourceRecord, "user");
2890
+ const workspaceIdFromResource = extractReferenceSlug(
2891
+ resourceRecord,
2892
+ "workspace"
2893
+ );
2894
+ const userProjectionItem = userIdFromResource !== void 0 ? buildMembershipUserProjectionItem({
2895
+ tenantId: context.tenantId,
2896
+ userId: userIdFromResource,
2897
+ workspaceId: workspaceIdFromResource,
2898
+ membershipId: id,
2899
+ summary,
2900
+ vid,
2901
+ lastUpdated,
2902
+ denormalizedTenantName,
2903
+ denormalizedUserName,
2904
+ denormalizedWorkspaceName
2905
+ }) : void 0;
2906
+ const workspaceProjectionItem = userIdFromResource !== void 0 && workspaceIdFromResource !== void 0 ? buildMembershipWorkspaceProjectionItem({
2907
+ tenantId: context.tenantId,
2908
+ workspaceId: workspaceIdFromResource,
2909
+ userId: userIdFromResource,
2910
+ membershipId: id,
2911
+ summary,
2912
+ vid,
2913
+ lastUpdated,
2914
+ denormalizedUserName
2915
+ }) : void 0;
2916
+ const canonicalItem = {
1640
2917
  tenantId: context.tenantId,
1641
2918
  id,
1642
2919
  resource: JSON.stringify(resource),
1643
2920
  summary,
1644
2921
  vid,
1645
2922
  lastUpdated,
1646
- linkedDataIdentityRef
1647
- }).go();
2923
+ linkedDataIdentityRef,
2924
+ denormalizedTenantName,
2925
+ denormalizedUserName
2926
+ };
2927
+ const triples = [
2928
+ { entity: "membership", action: "put", item: canonicalItem }
2929
+ ];
2930
+ if (userProjectionItem) {
2931
+ triples.push({
2932
+ entity: "membershipUserProjection",
2933
+ action: "put",
2934
+ item: userProjectionItem
2935
+ });
2936
+ }
2937
+ if (workspaceProjectionItem) {
2938
+ triples.push({
2939
+ entity: "membershipWorkspaceProjection",
2940
+ action: "put",
2941
+ item: workspaceProjectionItem
2942
+ });
2943
+ }
2944
+ await executeMultiWrite({ service, triples });
1648
2945
  return {
1649
2946
  id,
1650
2947
  resource,
@@ -1669,7 +2966,107 @@ async function getRoleByIdOperation(params) {
1669
2966
  }
1670
2967
 
1671
2968
  // src/data/operations/control/roleassignment/roleassignment-create-operation.ts
1672
- var import_types4 = require("@openhi/types");
2969
+ var import_types8 = require("@openhi/types");
2970
+
2971
+ // src/data/operations/control/roleassignment/roleassignment-user-projection.ts
2972
+ var import_types6 = require("@openhi/types");
2973
+ var MISSING_NAME_SENTINEL3 = "-";
2974
+ function buildRoleAssignmentUserProjectionSkTenantLane(params) {
2975
+ const normalizedRoleName = typeof params.denormalizedRoleName === "string" && params.denormalizedRoleName.length > 0 ? (0, import_types6.normalizeLabel)(params.denormalizedRoleName) : MISSING_NAME_SENTINEL3;
2976
+ return `ROLEASSIGNMENT#TENANT#${normalizedRoleName}#${params.roleId}#TID#${params.tenantId}#${params.roleAssignmentId}`;
2977
+ }
2978
+ function buildRoleAssignmentUserProjectionSkWorkspaceLane(params) {
2979
+ const normalizedRoleName = typeof params.denormalizedRoleName === "string" && params.denormalizedRoleName.length > 0 ? (0, import_types6.normalizeLabel)(params.denormalizedRoleName) : MISSING_NAME_SENTINEL3;
2980
+ return `ROLEASSIGNMENT#WORKSPACE#${normalizedRoleName}#${params.roleId}#TID#${params.tenantId}#WID#${params.workspaceId}#${params.roleAssignmentId}`;
2981
+ }
2982
+ function buildRoleAssignmentUserProjectionItem(input) {
2983
+ if (!input.userId || input.userId.length === 0) {
2984
+ return void 0;
2985
+ }
2986
+ if (!input.roleId || input.roleId.length === 0) {
2987
+ return void 0;
2988
+ }
2989
+ const hasWorkspace = typeof input.workspaceId === "string" && input.workspaceId.length > 0;
2990
+ const sk = hasWorkspace ? buildRoleAssignmentUserProjectionSkWorkspaceLane({
2991
+ tenantId: input.tenantId,
2992
+ workspaceId: input.workspaceId,
2993
+ roleId: input.roleId,
2994
+ roleAssignmentId: input.roleAssignmentId,
2995
+ denormalizedRoleName: input.denormalizedRoleName
2996
+ }) : buildRoleAssignmentUserProjectionSkTenantLane({
2997
+ tenantId: input.tenantId,
2998
+ roleId: input.roleId,
2999
+ roleAssignmentId: input.roleAssignmentId,
3000
+ denormalizedRoleName: input.denormalizedRoleName
3001
+ });
3002
+ return {
3003
+ userId: input.userId,
3004
+ sk,
3005
+ tenantId: input.tenantId,
3006
+ workspaceId: hasWorkspace ? input.workspaceId : void 0,
3007
+ roleId: input.roleId,
3008
+ roleAssignmentId: input.roleAssignmentId,
3009
+ summary: input.summary,
3010
+ vid: input.vid,
3011
+ lastUpdated: input.lastUpdated,
3012
+ denormalizedTenantName: input.denormalizedTenantName,
3013
+ denormalizedUserName: input.denormalizedUserName,
3014
+ denormalizedRoleName: input.denormalizedRoleName
3015
+ };
3016
+ }
3017
+ function extractReferenceSlug2(resource, fieldName) {
3018
+ const field = resource[fieldName];
3019
+ if (!field || typeof field !== "object") {
3020
+ return void 0;
3021
+ }
3022
+ const reference = field.reference;
3023
+ if (typeof reference !== "string" || reference.length === 0) {
3024
+ return void 0;
3025
+ }
3026
+ const slash = reference.lastIndexOf("/");
3027
+ const tail = slash >= 0 ? reference.slice(slash + 1) : reference;
3028
+ return tail.length > 0 ? tail : void 0;
3029
+ }
3030
+
3031
+ // src/data/operations/control/roleassignment/roleassignment-workspace-projection.ts
3032
+ var import_types7 = require("@openhi/types");
3033
+ var MISSING_NAME_SENTINEL4 = "-";
3034
+ function buildRoleAssignmentWorkspaceProjectionSk(params) {
3035
+ const normalizedUserName = typeof params.denormalizedUserName === "string" && params.denormalizedUserName.length > 0 ? (0, import_types7.normalizeLabel)(params.denormalizedUserName) : MISSING_NAME_SENTINEL4;
3036
+ return `ROLEASSIGNMENT#${params.roleId}#${normalizedUserName}#USER#${params.userId}#${params.roleAssignmentId}`;
3037
+ }
3038
+ function buildRoleAssignmentWorkspaceProjectionItem(input) {
3039
+ if (!input.workspaceId || input.workspaceId.length === 0) {
3040
+ return void 0;
3041
+ }
3042
+ if (!input.userId || input.userId.length === 0) {
3043
+ return void 0;
3044
+ }
3045
+ if (!input.roleId || input.roleId.length === 0) {
3046
+ return void 0;
3047
+ }
3048
+ const sk = buildRoleAssignmentWorkspaceProjectionSk({
3049
+ roleId: input.roleId,
3050
+ userId: input.userId,
3051
+ roleAssignmentId: input.roleAssignmentId,
3052
+ denormalizedUserName: input.denormalizedUserName
3053
+ });
3054
+ return {
3055
+ tenantId: input.tenantId,
3056
+ workspaceId: input.workspaceId,
3057
+ sk,
3058
+ userId: input.userId,
3059
+ roleId: input.roleId,
3060
+ roleAssignmentId: input.roleAssignmentId,
3061
+ summary: input.summary,
3062
+ vid: input.vid,
3063
+ lastUpdated: input.lastUpdated,
3064
+ denormalizedUserName: input.denormalizedUserName,
3065
+ denormalizedRoleName: input.denormalizedRoleName
3066
+ };
3067
+ }
3068
+
3069
+ // src/data/operations/control/roleassignment/roleassignment-create-operation.ts
1673
3070
  async function createRoleAssignmentOperation(params) {
1674
3071
  const { context, body, tableName } = params;
1675
3072
  const service = getDynamoControlService(tableName);
@@ -1678,15 +3075,80 @@ async function createRoleAssignmentOperation(params) {
1678
3075
  const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
1679
3076
  const vid = `1`;
1680
3077
  const resource = { resourceType: "RoleAssignment", id, ...parsedResource };
1681
- const summary = JSON.stringify((0, import_types4.extractSummary)(resource));
1682
- await service.entities.roleAssignment.put({
3078
+ const resourceRecord = resource;
3079
+ const denormalizedTenantName = extractDenormalizedReferenceDisplay(
3080
+ resourceRecord,
3081
+ "tenant"
3082
+ );
3083
+ const denormalizedUserName = extractDenormalizedReferenceDisplay(
3084
+ resourceRecord,
3085
+ "user"
3086
+ );
3087
+ const denormalizedRoleName = extractDenormalizedReferenceDisplay(
3088
+ resourceRecord,
3089
+ "role"
3090
+ );
3091
+ const summary = JSON.stringify((0, import_types8.extractSummary)(resource));
3092
+ const userIdFromResource = extractReferenceSlug2(resourceRecord, "user");
3093
+ const roleIdFromResource = extractReferenceSlug2(resourceRecord, "role");
3094
+ const workspaceIdFromResource = extractReferenceSlug2(
3095
+ resourceRecord,
3096
+ "workspace"
3097
+ );
3098
+ const userProjectionItem = userIdFromResource !== void 0 && roleIdFromResource !== void 0 ? buildRoleAssignmentUserProjectionItem({
3099
+ tenantId: context.tenantId,
3100
+ userId: userIdFromResource,
3101
+ workspaceId: workspaceIdFromResource,
3102
+ roleId: roleIdFromResource,
3103
+ roleAssignmentId: id,
3104
+ summary,
3105
+ vid,
3106
+ lastUpdated,
3107
+ denormalizedTenantName,
3108
+ denormalizedUserName,
3109
+ denormalizedRoleName
3110
+ }) : void 0;
3111
+ const workspaceProjectionItem = userIdFromResource !== void 0 && roleIdFromResource !== void 0 && workspaceIdFromResource !== void 0 ? buildRoleAssignmentWorkspaceProjectionItem({
3112
+ tenantId: context.tenantId,
3113
+ workspaceId: workspaceIdFromResource,
3114
+ userId: userIdFromResource,
3115
+ roleId: roleIdFromResource,
3116
+ roleAssignmentId: id,
3117
+ summary,
3118
+ vid,
3119
+ lastUpdated,
3120
+ denormalizedUserName,
3121
+ denormalizedRoleName
3122
+ }) : void 0;
3123
+ const canonicalItem = {
1683
3124
  tenantId: context.tenantId,
1684
3125
  id,
1685
3126
  resource: JSON.stringify(resource),
1686
3127
  summary,
1687
3128
  vid,
1688
- lastUpdated
1689
- }).go();
3129
+ lastUpdated,
3130
+ denormalizedTenantName,
3131
+ denormalizedUserName,
3132
+ denormalizedRoleName
3133
+ };
3134
+ const triples = [
3135
+ { entity: "roleAssignment", action: "put", item: canonicalItem }
3136
+ ];
3137
+ if (userProjectionItem) {
3138
+ triples.push({
3139
+ entity: "roleAssignmentUserProjection",
3140
+ action: "put",
3141
+ item: userProjectionItem
3142
+ });
3143
+ }
3144
+ if (workspaceProjectionItem) {
3145
+ triples.push({
3146
+ entity: "roleAssignmentWorkspaceProjection",
3147
+ action: "put",
3148
+ item: workspaceProjectionItem
3149
+ });
3150
+ }
3151
+ await executeMultiWrite({ service, triples });
1690
3152
  return {
1691
3153
  id,
1692
3154
  resource,
@@ -1695,7 +3157,7 @@ async function createRoleAssignmentOperation(params) {
1695
3157
  }
1696
3158
 
1697
3159
  // src/data/operations/control/tenant/tenant-create-operation.ts
1698
- var import_types5 = require("@openhi/types");
3160
+ var import_types9 = require("@openhi/types");
1699
3161
  async function createTenantOperation(params) {
1700
3162
  const { context, body, tableName } = params;
1701
3163
  const service = getDynamoControlService(tableName);
@@ -1704,7 +3166,7 @@ async function createTenantOperation(params) {
1704
3166
  const vid = lastUpdated.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36);
1705
3167
  const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
1706
3168
  const resource = { resourceType: "Tenant", id, ...parsedResource };
1707
- const summary = JSON.stringify((0, import_types5.extractSummary)(resource));
3169
+ const summary = JSON.stringify((0, import_types9.extractSummary)(resource));
1708
3170
  await service.entities.tenant.put({
1709
3171
  tenantId: id,
1710
3172
  id,
@@ -1717,13 +3179,13 @@ async function createTenantOperation(params) {
1717
3179
  }
1718
3180
 
1719
3181
  // src/data/operations/control/workspace/workspace-create-operation.ts
1720
- var import_types7 = require("@openhi/types");
3182
+ var import_types11 = require("@openhi/types");
1721
3183
 
1722
3184
  // src/data/dynamo/dynamo-data-service.ts
1723
- var import_electrodb10 = require("electrodb");
3185
+ var import_electrodb16 = require("electrodb");
1724
3186
 
1725
3187
  // src/data/dynamo/entities/data-entity-common.ts
1726
- var import_electrodb9 = require("electrodb");
3188
+ var import_electrodb15 = require("electrodb");
1727
3189
  var dataEntityAttributes = {
1728
3190
  /** Sort key. "CURRENT" for current version; version history in S3. */
1729
3191
  sk: {
@@ -1819,7 +3281,7 @@ var dataEntityAttributes = {
1819
3281
  }
1820
3282
  };
1821
3283
  function createDataEntity(entity, resourceTypeLabel) {
1822
- return new import_electrodb9.Entity({
3284
+ return new import_electrodb15.Entity({
1823
3285
  model: {
1824
3286
  entity,
1825
3287
  service: "data",
@@ -2733,26 +4195,28 @@ var dataPlaneEntities = {
2733
4195
  visionprescription: VisionPrescriptionEntity,
2734
4196
  verificationresult: VerificationResultEntity
2735
4197
  };
2736
- var dataPlaneService = new import_electrodb10.Service(dataPlaneEntities, {
4198
+ var dataPlaneService = new import_electrodb16.Service(dataPlaneEntities, {
2737
4199
  table: defaultTableName,
2738
4200
  client: dynamoClient
2739
4201
  });
2740
4202
  var DynamoDataService = {
2741
- entities: dataPlaneService.entities
4203
+ entities: dataPlaneService.entities,
4204
+ transaction: dataPlaneService.transaction
2742
4205
  };
2743
4206
  function getDynamoDataService(tableName) {
2744
4207
  const resolved = tableName ?? defaultTableName;
2745
- const service = new import_electrodb10.Service(dataPlaneEntities, {
4208
+ const service = new import_electrodb16.Service(dataPlaneEntities, {
2746
4209
  table: resolved,
2747
4210
  client: dynamoClient
2748
4211
  });
2749
4212
  return {
2750
- entities: service.entities
4213
+ entities: service.entities,
4214
+ transaction: service.transaction
2751
4215
  };
2752
4216
  }
2753
4217
 
2754
4218
  // src/data/operations/data-operations-common.ts
2755
- var import_types6 = require("@openhi/types");
4219
+ var import_types10 = require("@openhi/types");
2756
4220
 
2757
4221
  // src/lib/compression.ts
2758
4222
  var import_node_zlib = require("zlib");
@@ -2789,8 +4253,8 @@ async function createDataEntityRecord(entity, tenantId, workspaceId, id, resourc
2789
4253
  const lastUpdated = resourceWithAudit.meta?.lastUpdated ?? fallbackDate ?? (/* @__PURE__ */ new Date()).toISOString();
2790
4254
  const vid = lastUpdated.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36);
2791
4255
  const resourceLike = resourceWithAudit;
2792
- const summary = JSON.stringify((0, import_types6.extractSummary)(resourceLike));
2793
- const gsi1sk = (0, import_types6.extractSortKey)(resourceLike);
4256
+ const summary = JSON.stringify((0, import_types10.extractSummary)(resourceLike));
4257
+ const gsi1sk = (0, import_types10.extractSortKey)(resourceLike);
2794
4258
  await entity.put({
2795
4259
  sk: DATA_ENTITY_SK,
2796
4260
  tenantId,
@@ -2856,7 +4320,7 @@ async function createWorkspaceOperation(params) {
2856
4320
  const vid = lastUpdated.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36);
2857
4321
  const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
2858
4322
  const resource = { resourceType: "Workspace", id, ...parsedResource };
2859
- const summary = JSON.stringify((0, import_types7.extractSummary)(resource));
4323
+ const summary = JSON.stringify((0, import_types11.extractSummary)(resource));
2860
4324
  await service.entities.workspace.put({
2861
4325
  tenantId,
2862
4326
  id,
@@ -2884,9 +4348,9 @@ var errorMessage = (err) => {
2884
4348
  return String(err);
2885
4349
  };
2886
4350
  var idForRoleCode = (code) => {
2887
- for (const key of Object.keys(import_types8.PLATFORM_ROLE_IDS)) {
2888
- if (import_types8.PLATFORM_ROLE_CONCEPTS[key].code === code) {
2889
- return import_types8.PLATFORM_ROLE_IDS[key];
4351
+ for (const key of Object.keys(import_types12.PLATFORM_ROLE_IDS)) {
4352
+ if (import_types12.PLATFORM_ROLE_CONCEPTS[key].code === code) {
4353
+ return import_types12.PLATFORM_ROLE_IDS[key];
2890
4354
  }
2891
4355
  }
2892
4356
  throw new Error(`No id mapping for role code "${code}".`);
@@ -2901,7 +4365,7 @@ var verifySystemRolesExist = async () => {
2901
4365
  actorType: "internal-system",
2902
4366
  source: "step-function"
2903
4367
  };
2904
- for (const id of Object.values(import_types8.PLATFORM_ROLE_IDS)) {
4368
+ for (const id of Object.values(import_types12.PLATFORM_ROLE_IDS)) {
2905
4369
  try {
2906
4370
  await getRoleByIdOperation({ context: probeContext, id });
2907
4371
  } catch (err) {
@@ -2982,7 +4446,7 @@ var userResourceBody = (user, cognitoSub) => ({
2982
4446
  var upsertUser = async (context, user, cognitoSub) => {
2983
4447
  const service = getDynamoControlService();
2984
4448
  const resource = userResourceBody(user, cognitoSub);
2985
- const summary = JSON.stringify((0, import_types8.extractSummary)(resource));
4449
+ const summary = JSON.stringify((0, import_types12.extractSummary)(resource));
2986
4450
  await service.entities.user.put({
2987
4451
  id: user.id,
2988
4452
  cognitoSub,
@@ -3050,7 +4514,7 @@ var seedDemoGraph = async (params) => {
3050
4514
  ...baseContext,
3051
4515
  tenantId: PLATFORM_SCOPE_TENANT_ID
3052
4516
  };
3053
- const platformRoleCode = import_types8.PLATFORM_ROLE_CODE.SYSTEM_ADMIN;
4517
+ const platformRoleCode = import_types12.PLATFORM_ROLE_CODE.SYSTEM_ADMIN;
3054
4518
  const platformRaId = demoRoleAssignmentId(
3055
4519
  user.id,
3056
4520
  PLATFORM_SCOPE_TENANT_ID,