@openhi/constructs 0.0.111 → 0.0.113

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/lib/chunk-23PUSHBV.mjs +24 -0
  2. package/lib/chunk-23PUSHBV.mjs.map +1 -0
  3. package/lib/{chunk-7FUAMZOF.mjs → chunk-53OHXLIL.mjs} +3 -3
  4. package/lib/chunk-6NBGYGFL.mjs +1803 -0
  5. package/lib/chunk-6NBGYGFL.mjs.map +1 -0
  6. package/lib/chunk-7RZHFI77.mjs +22 -0
  7. package/lib/chunk-7RZHFI77.mjs.map +1 -0
  8. package/lib/{chunk-7Q2IJ2J5.mjs → chunk-CUUKXDB2.mjs} +6 -6
  9. package/lib/chunk-FYHBHHWK.mjs +47 -0
  10. package/lib/chunk-FYHBHHWK.mjs.map +1 -0
  11. package/lib/{chunk-MULKGFIJ.mjs → chunk-GBDIGTNV.mjs} +165 -10
  12. package/lib/chunk-GBDIGTNV.mjs.map +1 -0
  13. package/lib/chunk-HQ67J7BP.mjs +199 -0
  14. package/lib/chunk-HQ67J7BP.mjs.map +1 -0
  15. package/lib/{chunk-AJ3G3THO.mjs → chunk-KO64HPWQ.mjs} +2 -2
  16. package/lib/{chunk-BB5MK4L3.mjs → chunk-KSFC72TT.mjs} +3 -3
  17. package/lib/{chunk-2TPJ6HOF.mjs → chunk-NZRW7ROK.mjs} +72 -54
  18. package/lib/chunk-NZRW7ROK.mjs.map +1 -0
  19. package/lib/chunk-QJDHVMKT.mjs +117 -0
  20. package/lib/chunk-QJDHVMKT.mjs.map +1 -0
  21. package/lib/{chunk-IS4VQRI4.mjs → chunk-QMBJ4VHC.mjs} +12 -47
  22. package/lib/chunk-QMBJ4VHC.mjs.map +1 -0
  23. package/lib/chunk-TRY7JGWO.mjs +16 -0
  24. package/lib/chunk-TRY7JGWO.mjs.map +1 -0
  25. package/lib/chunk-W4KR4CSL.mjs +236 -0
  26. package/lib/chunk-W4KR4CSL.mjs.map +1 -0
  27. package/lib/{chunk-AGF3RAAZ.mjs → chunk-WPCBVDFZ.mjs} +2 -2
  28. package/lib/chunk-WQWFVEVX.mjs +66 -0
  29. package/lib/chunk-WQWFVEVX.mjs.map +1 -0
  30. package/lib/{chunk-SYBADQXI.mjs → chunk-ZM4GDHHC.mjs} +77 -2
  31. package/lib/chunk-ZM4GDHHC.mjs.map +1 -0
  32. package/lib/delete-chunk.handler.d.mts +29 -0
  33. package/lib/delete-chunk.handler.d.ts +29 -0
  34. package/lib/delete-chunk.handler.js +2716 -0
  35. package/lib/delete-chunk.handler.js.map +1 -0
  36. package/lib/delete-chunk.handler.mjs +47 -0
  37. package/lib/delete-chunk.handler.mjs.map +1 -0
  38. package/lib/events-CjS-sm0W.d.mts +107 -0
  39. package/lib/events-CjS-sm0W.d.ts +107 -0
  40. package/lib/events-Da_cFgtc.d.mts +208 -0
  41. package/lib/events-Da_cFgtc.d.ts +208 -0
  42. package/lib/finalize.handler.d.mts +35 -0
  43. package/lib/finalize.handler.d.ts +35 -0
  44. package/lib/finalize.handler.js +875 -0
  45. package/lib/finalize.handler.js.map +1 -0
  46. package/lib/finalize.handler.mjs +166 -0
  47. package/lib/finalize.handler.mjs.map +1 -0
  48. package/lib/index.d.mts +189 -2
  49. package/lib/index.d.ts +500 -3
  50. package/lib/index.js +1753 -174
  51. package/lib/index.js.map +1 -1
  52. package/lib/index.mjs +571 -17
  53. package/lib/index.mjs.map +1 -1
  54. package/lib/list-chunks.handler.d.mts +28 -0
  55. package/lib/list-chunks.handler.d.ts +28 -0
  56. package/lib/list-chunks.handler.js +2746 -0
  57. package/lib/list-chunks.handler.js.map +1 -0
  58. package/lib/list-chunks.handler.mjs +54 -0
  59. package/lib/list-chunks.handler.mjs.map +1 -0
  60. package/lib/platform-deploy-bridge.handler.js +76 -1
  61. package/lib/platform-deploy-bridge.handler.js.map +1 -1
  62. package/lib/platform-deploy-bridge.handler.mjs +1 -1
  63. package/lib/pre-token-generation.handler.js +1106 -155
  64. package/lib/pre-token-generation.handler.js.map +1 -1
  65. package/lib/pre-token-generation.handler.mjs +6 -4
  66. package/lib/pre-token-generation.handler.mjs.map +1 -1
  67. package/lib/provision-default-workspace.handler.js +1529 -142
  68. package/lib/provision-default-workspace.handler.js.map +1 -1
  69. package/lib/provision-default-workspace.handler.mjs +8 -4
  70. package/lib/provision-default-workspace.handler.mjs.map +1 -1
  71. package/lib/rename-finalize.handler.d.mts +30 -0
  72. package/lib/rename-finalize.handler.d.ts +30 -0
  73. package/lib/rename-finalize.handler.js +795 -0
  74. package/lib/rename-finalize.handler.js.map +1 -0
  75. package/lib/rename-finalize.handler.mjs +90 -0
  76. package/lib/rename-finalize.handler.mjs.map +1 -0
  77. package/lib/rename-list-targets.handler.d.mts +26 -0
  78. package/lib/rename-list-targets.handler.d.ts +26 -0
  79. package/lib/rename-list-targets.handler.js +2985 -0
  80. package/lib/rename-list-targets.handler.js.map +1 -0
  81. package/lib/rename-list-targets.handler.mjs +431 -0
  82. package/lib/rename-list-targets.handler.mjs.map +1 -0
  83. package/lib/rename-rewrite-chunk.handler.d.mts +35 -0
  84. package/lib/rename-rewrite-chunk.handler.d.ts +35 -0
  85. package/lib/rename-rewrite-chunk.handler.js +2021 -0
  86. package/lib/rename-rewrite-chunk.handler.js.map +1 -0
  87. package/lib/rename-rewrite-chunk.handler.mjs +27 -0
  88. package/lib/rename-rewrite-chunk.handler.mjs.map +1 -0
  89. package/lib/rest-api-lambda.handler.js +4021 -932
  90. package/lib/rest-api-lambda.handler.js.map +1 -1
  91. package/lib/rest-api-lambda.handler.mjs +1786 -80
  92. package/lib/rest-api-lambda.handler.mjs.map +1 -1
  93. package/lib/seed-demo-data.handler.js +1588 -124
  94. package/lib/seed-demo-data.handler.js.map +1 -1
  95. package/lib/seed-demo-data.handler.mjs +10 -6
  96. package/lib/seed-system-data.handler.js +1179 -155
  97. package/lib/seed-system-data.handler.js.map +1 -1
  98. package/lib/seed-system-data.handler.mjs +5 -4
  99. package/lib/seed-system-data.handler.mjs.map +1 -1
  100. package/package.json +2 -2
  101. package/lib/chunk-2TPJ6HOF.mjs.map +0 -1
  102. package/lib/chunk-IS4VQRI4.mjs.map +0 -1
  103. package/lib/chunk-MULKGFIJ.mjs.map +0 -1
  104. package/lib/chunk-QR5JVSCF.mjs +0 -862
  105. package/lib/chunk-QR5JVSCF.mjs.map +0 -1
  106. package/lib/chunk-SYBADQXI.mjs.map +0 -1
  107. /package/lib/{chunk-7FUAMZOF.mjs.map → chunk-53OHXLIL.mjs.map} +0 -0
  108. /package/lib/{chunk-7Q2IJ2J5.mjs.map → chunk-CUUKXDB2.mjs.map} +0 -0
  109. /package/lib/{chunk-AJ3G3THO.mjs.map → chunk-KO64HPWQ.mjs.map} +0 -0
  110. /package/lib/{chunk-BB5MK4L3.mjs.map → chunk-KSFC72TT.mjs.map} +0 -0
  111. /package/lib/{chunk-AGF3RAAZ.mjs.map → chunk-WPCBVDFZ.mjs.map} +0 -0
@@ -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
  } });
@@ -650,7 +725,7 @@ var SEED_SYSTEM_DATA_CONTROL_BUS_ENV_VAR = "CONTROL_EVENT_BUS_NAME";
650
725
  var import_types2 = require("@openhi/types");
651
726
 
652
727
  // src/data/dynamo/dynamo-control-service.ts
653
- var import_electrodb8 = require("electrodb");
728
+ var import_electrodb14 = require("electrodb");
654
729
 
655
730
  // src/data/dynamo/dynamo-client.ts
656
731
  var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
@@ -714,6 +789,60 @@ var gsi1skAttribute = {
714
789
  return label !== void 0 ? `${label}#${id}` : fallback;
715
790
  }
716
791
  };
792
+ function extractRoleId(resource) {
793
+ const flat = resource.roleId;
794
+ if (typeof flat === "string" && flat.length > 0) return flat;
795
+ const role = resource.role;
796
+ if (role && typeof role === "object") {
797
+ const reference = role.reference;
798
+ if (typeof reference === "string" && reference.length > 0) {
799
+ const slash = reference.lastIndexOf("/");
800
+ const tail = slash >= 0 ? reference.slice(slash + 1) : reference;
801
+ if (tail.length > 0) return tail;
802
+ }
803
+ }
804
+ return void 0;
805
+ }
806
+ var roleAssignmentGsi1skAttribute = {
807
+ type: "string",
808
+ watch: ["resource", "denormalizedUserName", "lastUpdated", "id"],
809
+ set: (_val, item) => {
810
+ const id = typeof item?.id === "string" ? item.id : "";
811
+ const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
812
+ const fallback = `${lastUpdated}#${id}`;
813
+ if (typeof item?.resource !== "string" || item.resource.length === 0) {
814
+ return fallback;
815
+ }
816
+ let parsed;
817
+ try {
818
+ parsed = JSON.parse(item.resource);
819
+ } catch {
820
+ return fallback;
821
+ }
822
+ if (!parsed || typeof parsed !== "object") return fallback;
823
+ const roleId = extractRoleId(parsed);
824
+ if (roleId === void 0) return fallback;
825
+ const denormalizedUserName = typeof item.denormalizedUserName === "string" ? item.denormalizedUserName : "";
826
+ const normalizedUserName = denormalizedUserName.length > 0 ? (0, import_types.normalizeLabel)(denormalizedUserName) : "";
827
+ if (normalizedUserName.length === 0) return fallback;
828
+ return `${roleId}#${normalizedUserName}#${id}`;
829
+ }
830
+ };
831
+ var membershipGsi1skAttribute = {
832
+ type: "string",
833
+ watch: ["denormalizedUserName", "lastUpdated", "id"],
834
+ set: (_val, item) => {
835
+ const id = typeof item?.id === "string" ? item.id : "";
836
+ const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
837
+ const fallback = `${lastUpdated}#${id}`;
838
+ const denormalizedUserName = typeof item?.denormalizedUserName === "string" ? item.denormalizedUserName : "";
839
+ const normalizedUserName = denormalizedUserName.length > 0 ? (0, import_types.normalizeLabel)(denormalizedUserName) : "";
840
+ if (normalizedUserName.length === 0) {
841
+ return fallback;
842
+ }
843
+ return `${normalizedUserName}#${id}`;
844
+ }
845
+ };
717
846
 
718
847
  // src/data/dynamo/entities/control/configuration-entity.ts
719
848
  var ConfigurationEntity = new import_electrodb.Entity({
@@ -840,218 +969,241 @@ var ConfigurationEntity = new import_electrodb.Entity({
840
969
  }
841
970
  });
842
971
 
843
- // src/data/dynamo/entities/control/membership-entity.ts
972
+ // src/data/dynamo/entities/control/configuration-user-projection-entity.ts
844
973
  var import_electrodb2 = require("electrodb");
845
- var MembershipEntity = new import_electrodb2.Entity({
974
+ var ConfigurationUserProjectionEntity = new import_electrodb2.Entity({
846
975
  model: {
847
- entity: "membership",
976
+ entity: "configurationUserProjection",
848
977
  service: "control",
849
978
  version: "01"
850
979
  },
851
980
  attributes: {
852
- /** Sort key sentinel. Always "CURRENT". */
853
- sk: {
854
- type: "string",
855
- required: true,
856
- default: "CURRENT"
857
- },
858
- /** Tenant in which the user has membership (required). */
859
- tenantId: {
981
+ /**
982
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
983
+ * base-table PK. Always required — the projection has no meaning
984
+ * outside a user partition.
985
+ */
986
+ userId: {
860
987
  type: "string",
861
988
  required: true
862
989
  },
863
- /** FHIR Resource.id; membership id. */
864
- id: {
990
+ /**
991
+ * Pre-composed sort key — built by the operations-layer projection
992
+ * writer via `buildConfigurationUserProjectionSk`. The entity stores
993
+ * the value verbatim so the SK grammar (pattern #10 user-scope) is
994
+ * owned by the operations layer, not duplicated here.
995
+ */
996
+ sk: {
865
997
  type: "string",
866
998
  required: true
867
999
  },
868
- /** Full Membership resource serialized as JSON string. */
869
- resource: {
1000
+ /**
1001
+ * Configuration canonical-record id. Stored as a discriminating
1002
+ * field so consumers can hydrate the canonical row via the
1003
+ * Configuration get-by-id operation when the projection's `summary`
1004
+ * is insufficient.
1005
+ */
1006
+ configurationId: {
870
1007
  type: "string",
871
1008
  required: true
872
1009
  },
873
1010
  /**
874
- * Summary projection (key display fields as JSON string: id, displayName, status).
875
- * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
1011
+ * Tenant the Configuration is associated with. The canonical row
1012
+ * keys off `(tenantId, workspaceId, userId, roleId)`; the projection
1013
+ * carries `tenantId` so consumers reconstructing the canonical PK
1014
+ * have the tenant segment without a hop.
876
1015
  */
877
- summary: {
1016
+ tenantId: {
878
1017
  type: "string",
879
1018
  required: true
880
1019
  },
881
- /** Version id (e.g. ULID). */
882
- vid: {
1020
+ /**
1021
+ * Scope marker. Always `"user"` on this projection — recorded
1022
+ * explicitly so future scope-bearing projections (workspace,
1023
+ * tenant, role) can share filter semantics in a unified
1024
+ * cross-projection list query if one ever lands.
1025
+ */
1026
+ scope: {
883
1027
  type: "string",
884
- required: true
1028
+ required: true,
1029
+ default: "user"
885
1030
  },
886
- lastUpdated: {
1031
+ /**
1032
+ * Configuration's `key` attribute (config category, e.g. endpoints,
1033
+ * branding, display). Mirrored from the canonical row so consumers
1034
+ * reading the projection get the natural display label without a
1035
+ * BatchGet hop. Doubles as the source of `<normalizedConfigName>` in
1036
+ * the SK.
1037
+ */
1038
+ displayName: {
887
1039
  type: "string",
888
- required: true
889
- },
890
- gsi1Shard: gsi1ShardAttribute,
891
- /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
892
- gsi1sk: gsi1skAttribute,
893
- deleted: {
894
- type: "boolean",
895
1040
  required: false
896
1041
  },
897
- bundleId: {
1042
+ /**
1043
+ * Summary projection (key display fields as JSON string) — mirrored
1044
+ * from the canonical Configuration row so user-partition queries do
1045
+ * not need a BatchGet hop.
1046
+ */
1047
+ summary: {
898
1048
  type: "string",
899
- required: false
1049
+ required: true
900
1050
  },
901
- msgId: {
1051
+ /** Version id mirrored from the canonical Configuration row. */
1052
+ vid: {
902
1053
  type: "string",
903
- required: false
1054
+ required: true
904
1055
  },
905
- /**
906
- * Denormalized `linked-data-identity` Reference (e.g. `Practitioner/abc`).
907
- * Populated from the FHIR extension on the Membership resource at write
908
- * time so future GSIs can index data-plane identity lookups without
909
- * deserializing the full resource JSON. See ADR 2026-03-13-02 §6.
910
- */
911
- linkedDataIdentityRef: {
1056
+ /** Last-updated timestamp mirrored from the canonical Configuration row. */
1057
+ lastUpdated: {
912
1058
  type: "string",
913
- required: false
1059
+ required: true
914
1060
  }
915
1061
  },
916
1062
  indexes: {
917
- /** Base table: PK = TID#<tenantId>#MEMBERSHIP#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
1063
+ /**
1064
+ * Base table: PK = USER#ID#<userId>, SK = operation-supplied. A
1065
+ * single `Query(PK = USER#ID#<userId>, SK begins_with
1066
+ * 'CONFIGURATION#')` returns the user's user-scoped Configurations
1067
+ * sorted by `<normalizedConfigName>` (then `<configurationId>` as
1068
+ * the tiebreaker).
1069
+ */
918
1070
  record: {
919
1071
  pk: {
920
1072
  field: "PK",
921
- composite: ["tenantId", "id"],
922
- template: "TID#${tenantId}#MEMBERSHIP#ID#${id}"
1073
+ composite: ["userId"],
1074
+ template: "USER#ID#${userId}"
923
1075
  },
924
1076
  sk: {
925
1077
  field: "SK",
1078
+ casing: "none",
926
1079
  composite: ["sk"],
927
1080
  template: "${sk}"
928
1081
  }
929
- },
930
- /**
931
- * GSI1 — Unified Sharded List per ADR-011: list all Memberships for a tenant across the
932
- * four shards. Membership is tenant-scoped only, so `WID#-` is a sentinel.
933
- * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
934
- * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
935
- * normalized label and ISO-8601 `T`/`Z`.
936
- */
937
- gsi1: {
938
- index: "GSI1",
939
- pk: {
940
- field: "GSI1PK",
941
- composite: ["tenantId", "gsi1Shard"],
942
- template: "TID#${tenantId}#WID#-#RT#Membership#SHARD#${gsi1Shard}"
943
- },
944
- sk: {
945
- field: "GSI1SK",
946
- casing: "none",
947
- composite: ["gsi1sk"],
948
- template: "${gsi1sk}"
949
- }
950
1082
  }
951
1083
  }
952
1084
  });
953
1085
 
954
- // src/data/dynamo/entities/control/role-entity.ts
1086
+ // src/data/dynamo/entities/control/configuration-workspace-projection-entity.ts
955
1087
  var import_electrodb3 = require("electrodb");
956
- var RoleEntity = new import_electrodb3.Entity({
1088
+ var ConfigurationWorkspaceProjectionEntity = new import_electrodb3.Entity({
957
1089
  model: {
958
- entity: "role",
1090
+ entity: "configurationWorkspaceProjection",
959
1091
  service: "control",
960
1092
  version: "01"
961
1093
  },
962
1094
  attributes: {
963
- /** Sort key sentinel. Always "CURRENT". */
964
- sk: {
965
- type: "string",
966
- required: true,
967
- default: "CURRENT"
968
- },
969
- /** FHIR Resource.id; role id. */
970
- id: {
1095
+ /**
1096
+ * Tenant the workspace belongs to. Renders as the leading segment
1097
+ * of the base-table PK. Always required — the workspace partition
1098
+ * is tenant-scoped per ADR-011.
1099
+ */
1100
+ tenantId: {
971
1101
  type: "string",
972
1102
  required: true
973
1103
  },
974
- /** Full Role resource serialized as JSON string. */
975
- resource: {
1104
+ /**
1105
+ * Workspace partition discriminator. Renders as the trailing
1106
+ * segment of the base-table PK
1107
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
1108
+ * the projection has no meaning outside a workspace partition.
1109
+ */
1110
+ workspaceId: {
976
1111
  type: "string",
977
1112
  required: true
978
1113
  },
979
1114
  /**
980
- * Summary projection (key display fields as JSON string: id, displayName, status).
981
- * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
1115
+ * Pre-composed sort key built by the operations-layer projection
1116
+ * writer via `buildConfigurationWorkspaceProjectionSk`. The entity
1117
+ * stores the value verbatim so the SK grammar (pattern #10
1118
+ * workspace-scope) is owned by the operations layer, not
1119
+ * duplicated here.
982
1120
  */
983
- summary: {
1121
+ sk: {
984
1122
  type: "string",
985
1123
  required: true
986
1124
  },
987
- /** Version id (e.g. ULID). */
988
- vid: {
1125
+ /**
1126
+ * Configuration canonical-record id. Stored as a discriminating
1127
+ * field so consumers can hydrate the canonical row via the
1128
+ * Configuration get-by-id operation when the projection's `summary`
1129
+ * is insufficient.
1130
+ */
1131
+ configurationId: {
989
1132
  type: "string",
990
1133
  required: true
991
1134
  },
992
- lastUpdated: {
1135
+ /**
1136
+ * Scope marker. Always `"workspace"` on this projection — recorded
1137
+ * explicitly so future scope-bearing projections (user, tenant,
1138
+ * role) can share filter semantics in a unified cross-projection
1139
+ * list query if one ever lands.
1140
+ */
1141
+ scope: {
993
1142
  type: "string",
994
- required: true
1143
+ required: true,
1144
+ default: "workspace"
995
1145
  },
996
- gsi1Shard: gsi1ShardAttribute,
997
- /** Derived GSI1 sort key name-based when extractable; else `<lastUpdated>#<id>`. */
998
- gsi1sk: gsi1skAttribute,
999
- deleted: {
1000
- type: "boolean",
1146
+ /**
1147
+ * Configuration's `key` attribute (config category, e.g. endpoints,
1148
+ * branding, display). Mirrored from the canonical row so consumers
1149
+ * reading the projection get the natural display label without a
1150
+ * BatchGet hop. Doubles as the source of `<normalizedConfigName>`
1151
+ * in the SK.
1152
+ */
1153
+ displayName: {
1154
+ type: "string",
1001
1155
  required: false
1002
1156
  },
1003
- bundleId: {
1157
+ /**
1158
+ * Summary projection (key display fields as JSON string) — mirrored
1159
+ * from the canonical Configuration row so workspace-partition
1160
+ * queries do not need a BatchGet hop.
1161
+ */
1162
+ summary: {
1004
1163
  type: "string",
1005
- required: false
1164
+ required: true
1006
1165
  },
1007
- msgId: {
1166
+ /** Version id mirrored from the canonical Configuration row. */
1167
+ vid: {
1008
1168
  type: "string",
1009
- required: false
1169
+ required: true
1170
+ },
1171
+ /** Last-updated timestamp mirrored from the canonical Configuration row. */
1172
+ lastUpdated: {
1173
+ type: "string",
1174
+ required: true
1010
1175
  }
1011
1176
  },
1012
1177
  indexes: {
1013
- /** Base table: PK = ROLE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
1178
+ /**
1179
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
1180
+ * SK = operation-supplied. A single
1181
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'CONFIGURATION#')`
1182
+ * returns the workspace's workspace-scoped Configurations sorted by
1183
+ * `<normalizedConfigName>` (then `<configurationId>` as the
1184
+ * tiebreaker).
1185
+ */
1014
1186
  record: {
1015
1187
  pk: {
1016
1188
  field: "PK",
1017
- composite: ["id"],
1018
- template: "ROLE#ID#${id}"
1189
+ composite: ["tenantId", "workspaceId"],
1190
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
1019
1191
  },
1020
1192
  sk: {
1021
1193
  field: "SK",
1194
+ casing: "none",
1022
1195
  composite: ["sk"],
1023
1196
  template: "${sk}"
1024
1197
  }
1025
- },
1026
- /**
1027
- * GSI1 — Unified Sharded List per ADR-011: list all Roles across the four shards.
1028
- * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#Role#SHARD#<n>`.
1029
- * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
1030
- * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
1031
- * normalized label and ISO-8601 `T`/`Z`.
1032
- */
1033
- gsi1: {
1034
- index: "GSI1",
1035
- pk: {
1036
- field: "GSI1PK",
1037
- composite: ["gsi1Shard"],
1038
- template: "TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}"
1039
- },
1040
- sk: {
1041
- field: "GSI1SK",
1042
- casing: "none",
1043
- composite: ["gsi1sk"],
1044
- template: "${gsi1sk}"
1045
- }
1046
1198
  }
1047
1199
  }
1048
1200
  });
1049
1201
 
1050
- // src/data/dynamo/entities/control/roleassignment-entity.ts
1202
+ // src/data/dynamo/entities/control/membership-entity.ts
1051
1203
  var import_electrodb4 = require("electrodb");
1052
- var RoleAssignmentEntity = new import_electrodb4.Entity({
1204
+ var MembershipEntity = new import_electrodb4.Entity({
1053
1205
  model: {
1054
- entity: "roleassignment",
1206
+ entity: "membership",
1055
1207
  service: "control",
1056
1208
  version: "01"
1057
1209
  },
@@ -1062,17 +1214,17 @@ var RoleAssignmentEntity = new import_electrodb4.Entity({
1062
1214
  required: true,
1063
1215
  default: "CURRENT"
1064
1216
  },
1065
- /** Tenant in which the role assignment applies (required). */
1217
+ /** Tenant in which the user has membership (required). */
1066
1218
  tenantId: {
1067
1219
  type: "string",
1068
1220
  required: true
1069
1221
  },
1070
- /** FHIR Resource.id; role assignment id. */
1222
+ /** FHIR Resource.id; membership id. */
1071
1223
  id: {
1072
1224
  type: "string",
1073
1225
  required: true
1074
1226
  },
1075
- /** Full RoleAssignment resource serialized as JSON string. */
1227
+ /** Full Membership resource serialized as JSON string. */
1076
1228
  resource: {
1077
1229
  type: "string",
1078
1230
  required: true
@@ -1095,8 +1247,14 @@ var RoleAssignmentEntity = new import_electrodb4.Entity({
1095
1247
  required: true
1096
1248
  },
1097
1249
  gsi1Shard: gsi1ShardAttribute,
1098
- /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
1099
- gsi1sk: gsi1skAttribute,
1250
+ /**
1251
+ * Derived GSI1 sort key — `<normalizedUserName>#<id>` per ADR-018
1252
+ * pattern #1 so a GSI1 query partitioned on the tenant range-scans
1253
+ * by user-name prefix and returns memberships sorted by user name.
1254
+ * Falls back to `<lastUpdated>#<id>` when `denormalizedUserName`
1255
+ * is missing.
1256
+ */
1257
+ gsi1sk: membershipGsi1skAttribute,
1100
1258
  deleted: {
1101
1259
  type: "boolean",
1102
1260
  required: false
@@ -1108,28 +1266,566 @@ var RoleAssignmentEntity = new import_electrodb4.Entity({
1108
1266
  msgId: {
1109
1267
  type: "string",
1110
1268
  required: false
1111
- }
1112
- },
1113
- indexes: {
1114
- /** Base table: PK = TID#<tenantId>#ROLEASSIGNMENT#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
1115
- record: {
1116
- pk: {
1117
- field: "PK",
1118
- composite: ["tenantId", "id"],
1119
- template: "TID#${tenantId}#ROLEASSIGNMENT#ID#${id}"
1120
- },
1121
- sk: {
1122
- field: "SK",
1123
- composite: ["sk"],
1124
- template: "${sk}"
1269
+ },
1270
+ /**
1271
+ * Denormalized `linked-data-identity` Reference (e.g. `Practitioner/abc`).
1272
+ * Populated from the FHIR extension on the Membership resource at write
1273
+ * time so future GSIs can index data-plane identity lookups without
1274
+ * deserializing the full resource JSON. See ADR 2026-03-13-02 §6.
1275
+ */
1276
+ linkedDataIdentityRef: {
1277
+ type: "string",
1278
+ required: false
1279
+ },
1280
+ /**
1281
+ * Denormalized display name of the linked Tenant, captured at row
1282
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1283
+ * adjacency-list projection SKs (pattern #3 — `MEMBERSHIP#TENANT#<normalizedTenantName>#…`)
1284
+ * can be composed from a top-level field instead of digging into the
1285
+ * `resource` JSON. Optional on the schema so pre-TR-024 rows do not
1286
+ * break; the operations-layer multi-write helper (#1010) makes the
1287
+ * field load-bearing at write time per TR-024 rule 2 (write-time
1288
+ * source = canonical Tenant.displayName).
1289
+ * @see TR-024 — Denormalized display-name attributes
1290
+ */
1291
+ denormalizedTenantName: {
1292
+ type: "string",
1293
+ required: false
1294
+ },
1295
+ /**
1296
+ * Denormalized display name of the linked User, captured at row
1297
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1298
+ * adjacency-list canonical-record GSI1SK (pattern #1 —
1299
+ * `<normalizedUserName>#<id>`) and workspace-projection SK (pattern #2)
1300
+ * can be composed from a top-level field. Optional on the schema so
1301
+ * pre-TR-024 rows do not break; the operations-layer multi-write helper
1302
+ * (#1010) makes the field load-bearing at write time per TR-024 rule 2
1303
+ * (write-time source = canonical User.displayName).
1304
+ * @see TR-024 — Denormalized display-name attributes
1305
+ */
1306
+ denormalizedUserName: {
1307
+ type: "string",
1308
+ required: false
1309
+ }
1310
+ },
1311
+ indexes: {
1312
+ /** Base table: PK = TID#<tenantId>#MEMBERSHIP#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
1313
+ record: {
1314
+ pk: {
1315
+ field: "PK",
1316
+ composite: ["tenantId", "id"],
1317
+ template: "TID#${tenantId}#MEMBERSHIP#ID#${id}"
1318
+ },
1319
+ sk: {
1320
+ field: "SK",
1321
+ composite: ["sk"],
1322
+ template: "${sk}"
1323
+ }
1324
+ },
1325
+ /**
1326
+ * GSI1 — Unified Sharded List per ADR-011: list all Memberships for a tenant across the
1327
+ * four shards. Membership is tenant-scoped only, so `WID#-` is a sentinel.
1328
+ * SK is derived via `membershipGsi1skAttribute` — composes
1329
+ * `<normalizedUserName>#<id>` per ADR-018 pattern #1 (users in a
1330
+ * tenant, sorted by user name); falls back to `<lastUpdated>#<id>`
1331
+ * when `denormalizedUserName` is missing. `casing: "none"` preserves
1332
+ * the normalized label and ISO-8601 `T`/`Z`.
1333
+ */
1334
+ gsi1: {
1335
+ index: "GSI1",
1336
+ pk: {
1337
+ field: "GSI1PK",
1338
+ composite: ["tenantId", "gsi1Shard"],
1339
+ template: "TID#${tenantId}#WID#-#RT#Membership#SHARD#${gsi1Shard}"
1340
+ },
1341
+ sk: {
1342
+ field: "GSI1SK",
1343
+ casing: "none",
1344
+ composite: ["gsi1sk"],
1345
+ template: "${gsi1sk}"
1346
+ }
1347
+ }
1348
+ }
1349
+ });
1350
+
1351
+ // src/data/dynamo/entities/control/membership-user-projection-entity.ts
1352
+ var import_electrodb5 = require("electrodb");
1353
+ var MembershipUserProjectionEntity = new import_electrodb5.Entity({
1354
+ model: {
1355
+ entity: "membershipUserProjection",
1356
+ service: "control",
1357
+ version: "01"
1358
+ },
1359
+ attributes: {
1360
+ /**
1361
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
1362
+ * base-table PK. Always required — the projection has no meaning
1363
+ * outside a user partition.
1364
+ */
1365
+ userId: {
1366
+ type: "string",
1367
+ required: true
1368
+ },
1369
+ /**
1370
+ * Pre-composed sort key — built by the operations-layer projection
1371
+ * writer via `buildMembershipUserProjectionSk*` helpers. The entity
1372
+ * stores the value verbatim so the SK grammar (patterns #3 and #4)
1373
+ * is owned by the operations layer, not duplicated here.
1374
+ */
1375
+ sk: {
1376
+ type: "string",
1377
+ required: true
1378
+ },
1379
+ /** Tenant in which the membership applies. Always required. */
1380
+ tenantId: {
1381
+ type: "string",
1382
+ required: true
1383
+ },
1384
+ /**
1385
+ * Workspace the membership scopes to. Present iff the projection
1386
+ * row is a pattern-#4 workspace sub-lane row; absent for pattern-#3
1387
+ * tenant sub-lane rows.
1388
+ */
1389
+ workspaceId: {
1390
+ type: "string",
1391
+ required: false
1392
+ },
1393
+ /**
1394
+ * Membership canonical-record id. Stored as a discriminating field
1395
+ * so consumers can hydrate the canonical row via
1396
+ * `MembershipEntity.get({ tenantId, id: membershipId })` when the
1397
+ * projection's `summary` is insufficient.
1398
+ */
1399
+ membershipId: {
1400
+ type: "string",
1401
+ required: true
1402
+ },
1403
+ /**
1404
+ * Summary projection (key display fields as JSON string: id,
1405
+ * displayName, status) — mirrored from the canonical Membership row
1406
+ * so user-partition queries do not need a BatchGet hop.
1407
+ */
1408
+ summary: {
1409
+ type: "string",
1410
+ required: true
1411
+ },
1412
+ /** Version id mirrored from the canonical Membership row. */
1413
+ vid: {
1414
+ type: "string",
1415
+ required: true
1416
+ },
1417
+ /** Last-updated timestamp mirrored from the canonical Membership row. */
1418
+ lastUpdated: {
1419
+ type: "string",
1420
+ required: true
1421
+ },
1422
+ /**
1423
+ * Denormalized Tenant display name — required to compose pattern-#3
1424
+ * SK (`MEMBERSHIP#TENANT#<normalizedTenantName>#…`). Optional on the
1425
+ * schema because pre-TR-024 rows may not carry a display name; the
1426
+ * operations layer falls back gracefully when missing.
1427
+ */
1428
+ denormalizedTenantName: {
1429
+ type: "string",
1430
+ required: false
1431
+ },
1432
+ /**
1433
+ * Denormalized User display name — mirrored from the canonical
1434
+ * Membership row per TR-024 rule 3 (canonical-record symmetry).
1435
+ * Carried on the projection so consumers can render the user's
1436
+ * display name without a hop to the User record.
1437
+ */
1438
+ denormalizedUserName: {
1439
+ type: "string",
1440
+ required: false
1441
+ },
1442
+ /**
1443
+ * Denormalized Workspace display name — required to compose
1444
+ * pattern-#4 SK (`MEMBERSHIP#WORKSPACE#TID#<tenantId>#<normalizedWorkspaceName>#…`).
1445
+ * Optional on the schema (TR-024 § Open Item #4 defers a formal
1446
+ * Workspace-rename cascade); the operations layer falls back to a
1447
+ * sentinel when missing so the SK still has a valid shape.
1448
+ */
1449
+ denormalizedWorkspaceName: {
1450
+ type: "string",
1451
+ required: false
1452
+ }
1453
+ },
1454
+ indexes: {
1455
+ /**
1456
+ * Base table: PK = USER#ID#<userId>, SK = operation-supplied.
1457
+ * Both pattern #3 and pattern #4 use this same index — the SK string
1458
+ * encodes the lane discriminator (`MEMBERSHIP#TENANT#…` vs
1459
+ * `MEMBERSHIP#WORKSPACE#…`) so a single `Query(PK = USER#ID#<userId>,
1460
+ * SK begins_with 'MEMBERSHIP#')` returns both lanes interleaved.
1461
+ */
1462
+ record: {
1463
+ pk: {
1464
+ field: "PK",
1465
+ composite: ["userId"],
1466
+ template: "USER#ID#${userId}"
1467
+ },
1468
+ sk: {
1469
+ field: "SK",
1470
+ casing: "none",
1471
+ composite: ["sk"],
1472
+ template: "${sk}"
1473
+ }
1474
+ }
1475
+ }
1476
+ });
1477
+
1478
+ // src/data/dynamo/entities/control/membership-workspace-projection-entity.ts
1479
+ var import_electrodb6 = require("electrodb");
1480
+ var MembershipWorkspaceProjectionEntity = new import_electrodb6.Entity({
1481
+ model: {
1482
+ entity: "membershipWorkspaceProjection",
1483
+ service: "control",
1484
+ version: "01"
1485
+ },
1486
+ attributes: {
1487
+ /**
1488
+ * Tenant the workspace belongs to. Renders as the leading segment
1489
+ * of the base-table PK. Always required — the workspace partition
1490
+ * is tenant-scoped per ADR-011.
1491
+ */
1492
+ tenantId: {
1493
+ type: "string",
1494
+ required: true
1495
+ },
1496
+ /**
1497
+ * Workspace partition discriminator. Renders as the trailing
1498
+ * segment of the base-table PK
1499
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
1500
+ * the projection has no meaning outside a workspace partition.
1501
+ */
1502
+ workspaceId: {
1503
+ type: "string",
1504
+ required: true
1505
+ },
1506
+ /**
1507
+ * Pre-composed sort key — built by the operations-layer projection
1508
+ * writer via `buildMembershipWorkspaceProjectionSk`. The entity
1509
+ * stores the value verbatim so the SK grammar (pattern #2) is
1510
+ * owned by the operations layer, not duplicated here.
1511
+ */
1512
+ sk: {
1513
+ type: "string",
1514
+ required: true
1515
+ },
1516
+ /**
1517
+ * User the membership links. Stored as a discriminating field so
1518
+ * consumers can hydrate the canonical User row via
1519
+ * `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
1520
+ * projection's `summary` is insufficient.
1521
+ */
1522
+ userId: {
1523
+ type: "string",
1524
+ required: true
1525
+ },
1526
+ /**
1527
+ * Membership canonical-record id. Stored as a discriminating field
1528
+ * so consumers can hydrate the canonical row via
1529
+ * `MembershipEntity.get({ tenantId, id: membershipId })` when the
1530
+ * projection's `summary` is insufficient.
1531
+ */
1532
+ membershipId: {
1533
+ type: "string",
1534
+ required: true
1535
+ },
1536
+ /**
1537
+ * Summary projection (key display fields as JSON string: id,
1538
+ * displayName, status) — mirrored from the canonical Membership row
1539
+ * so workspace-partition queries do not need a BatchGet hop.
1540
+ */
1541
+ summary: {
1542
+ type: "string",
1543
+ required: true
1544
+ },
1545
+ /** Version id mirrored from the canonical Membership row. */
1546
+ vid: {
1547
+ type: "string",
1548
+ required: true
1549
+ },
1550
+ /** Last-updated timestamp mirrored from the canonical Membership row. */
1551
+ lastUpdated: {
1552
+ type: "string",
1553
+ required: true
1554
+ },
1555
+ /**
1556
+ * Denormalized User display name — required to compose the
1557
+ * pattern-#2 SK (`MEMBERSHIP#<normalizedUserName>#…`). Optional on
1558
+ * the schema because pre-TR-024 rows may not carry a display name;
1559
+ * the operations layer falls back to a sentinel when missing so
1560
+ * the SK still has a valid shape. The TR-023 rename-cascade
1561
+ * pipeline rewrites the SK on a User rename.
1562
+ */
1563
+ denormalizedUserName: {
1564
+ type: "string",
1565
+ required: false
1566
+ }
1567
+ },
1568
+ indexes: {
1569
+ /**
1570
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
1571
+ * SK = operation-supplied. Pattern #2 uses this index — the SK
1572
+ * encodes the entity-type prefix (`MEMBERSHIP#…`) so a
1573
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'MEMBERSHIP#')`
1574
+ * returns every member projection for the workspace in normalized-
1575
+ * user-name sort order.
1576
+ */
1577
+ record: {
1578
+ pk: {
1579
+ field: "PK",
1580
+ composite: ["tenantId", "workspaceId"],
1581
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
1582
+ },
1583
+ sk: {
1584
+ field: "SK",
1585
+ casing: "none",
1586
+ composite: ["sk"],
1587
+ template: "${sk}"
1588
+ }
1589
+ }
1590
+ }
1591
+ });
1592
+
1593
+ // src/data/dynamo/entities/control/role-entity.ts
1594
+ var import_electrodb7 = require("electrodb");
1595
+ var RoleEntity = new import_electrodb7.Entity({
1596
+ model: {
1597
+ entity: "role",
1598
+ service: "control",
1599
+ version: "01"
1600
+ },
1601
+ attributes: {
1602
+ /** Sort key sentinel. Always "CURRENT". */
1603
+ sk: {
1604
+ type: "string",
1605
+ required: true,
1606
+ default: "CURRENT"
1607
+ },
1608
+ /** FHIR Resource.id; role id. */
1609
+ id: {
1610
+ type: "string",
1611
+ required: true
1612
+ },
1613
+ /** Full Role resource serialized as JSON string. */
1614
+ resource: {
1615
+ type: "string",
1616
+ required: true
1617
+ },
1618
+ /**
1619
+ * Summary projection (key display fields as JSON string: id, displayName, status).
1620
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
1621
+ */
1622
+ summary: {
1623
+ type: "string",
1624
+ required: true
1625
+ },
1626
+ /** Version id (e.g. ULID). */
1627
+ vid: {
1628
+ type: "string",
1629
+ required: true
1630
+ },
1631
+ lastUpdated: {
1632
+ type: "string",
1633
+ required: true
1634
+ },
1635
+ gsi1Shard: gsi1ShardAttribute,
1636
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
1637
+ gsi1sk: gsi1skAttribute,
1638
+ deleted: {
1639
+ type: "boolean",
1640
+ required: false
1641
+ },
1642
+ bundleId: {
1643
+ type: "string",
1644
+ required: false
1645
+ },
1646
+ msgId: {
1647
+ type: "string",
1648
+ required: false
1649
+ }
1650
+ },
1651
+ indexes: {
1652
+ /** Base table: PK = ROLE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
1653
+ record: {
1654
+ pk: {
1655
+ field: "PK",
1656
+ composite: ["id"],
1657
+ template: "ROLE#ID#${id}"
1658
+ },
1659
+ sk: {
1660
+ field: "SK",
1661
+ composite: ["sk"],
1662
+ template: "${sk}"
1663
+ }
1664
+ },
1665
+ /**
1666
+ * GSI1 — Unified Sharded List per ADR-011: list all Roles across the four shards.
1667
+ * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#Role#SHARD#<n>`.
1668
+ * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
1669
+ * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
1670
+ * normalized label and ISO-8601 `T`/`Z`.
1671
+ */
1672
+ gsi1: {
1673
+ index: "GSI1",
1674
+ pk: {
1675
+ field: "GSI1PK",
1676
+ composite: ["gsi1Shard"],
1677
+ template: "TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}"
1678
+ },
1679
+ sk: {
1680
+ field: "GSI1SK",
1681
+ casing: "none",
1682
+ composite: ["gsi1sk"],
1683
+ template: "${gsi1sk}"
1684
+ }
1685
+ }
1686
+ }
1687
+ });
1688
+
1689
+ // src/data/dynamo/entities/control/roleassignment-entity.ts
1690
+ var import_electrodb8 = require("electrodb");
1691
+ var RoleAssignmentEntity = new import_electrodb8.Entity({
1692
+ model: {
1693
+ entity: "roleassignment",
1694
+ service: "control",
1695
+ version: "01"
1696
+ },
1697
+ attributes: {
1698
+ /** Sort key sentinel. Always "CURRENT". */
1699
+ sk: {
1700
+ type: "string",
1701
+ required: true,
1702
+ default: "CURRENT"
1703
+ },
1704
+ /** Tenant in which the role assignment applies (required). */
1705
+ tenantId: {
1706
+ type: "string",
1707
+ required: true
1708
+ },
1709
+ /** FHIR Resource.id; role assignment id. */
1710
+ id: {
1711
+ type: "string",
1712
+ required: true
1713
+ },
1714
+ /** Full RoleAssignment resource serialized as JSON string. */
1715
+ resource: {
1716
+ type: "string",
1717
+ required: true
1718
+ },
1719
+ /**
1720
+ * Summary projection (key display fields as JSON string: id, displayName, status).
1721
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
1722
+ */
1723
+ summary: {
1724
+ type: "string",
1725
+ required: true
1726
+ },
1727
+ /** Version id (e.g. ULID). */
1728
+ vid: {
1729
+ type: "string",
1730
+ required: true
1731
+ },
1732
+ lastUpdated: {
1733
+ type: "string",
1734
+ required: true
1735
+ },
1736
+ gsi1Shard: gsi1ShardAttribute,
1737
+ /**
1738
+ * Derived GSI1 sort key — discriminator-first
1739
+ * `<roleId>#<normalizedUserName>#<id>` per ADR-018 pattern #8 so a
1740
+ * GSI1 query partitioned on the tenant can `begins_with('<roleId>#')`
1741
+ * to enumerate every user assigned to a given role, sorted by user
1742
+ * name. Falls back to `<lastUpdated>#<id>` when either component is
1743
+ * missing.
1744
+ */
1745
+ gsi1sk: roleAssignmentGsi1skAttribute,
1746
+ deleted: {
1747
+ type: "boolean",
1748
+ required: false
1749
+ },
1750
+ bundleId: {
1751
+ type: "string",
1752
+ required: false
1753
+ },
1754
+ msgId: {
1755
+ type: "string",
1756
+ required: false
1757
+ },
1758
+ /**
1759
+ * Denormalized display name of the linked Tenant, captured at row
1760
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1761
+ * adjacency-list user-projection SK (pattern #5 —
1762
+ * `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#<roleId>#TID#<tenantId>#<id>`)
1763
+ * can be composed from a top-level field instead of digging into the
1764
+ * `resource` JSON. Optional on the schema so pre-TR-024 rows do not
1765
+ * break; the operations-layer multi-write helper (#1010) makes the
1766
+ * field load-bearing at write time per TR-024 rule 2 (write-time
1767
+ * source = canonical Tenant.displayName).
1768
+ * @see TR-024 — Denormalized display-name attributes
1769
+ */
1770
+ denormalizedTenantName: {
1771
+ type: "string",
1772
+ required: false
1773
+ },
1774
+ /**
1775
+ * Denormalized display name of the linked User, captured at row
1776
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1777
+ * adjacency-list canonical-record GSI1SK (pattern #8 —
1778
+ * `<roleId>#<normalizedUserName>#<id>`) and workspace-projection SK
1779
+ * (pattern #9) can be composed from a top-level field. Optional on
1780
+ * the schema so pre-TR-024 rows do not break; the operations-layer
1781
+ * multi-write helper (#1010) makes the field load-bearing at write
1782
+ * time per TR-024 rule 2 (write-time source = canonical
1783
+ * User.displayName).
1784
+ * @see TR-024 — Denormalized display-name attributes
1785
+ */
1786
+ denormalizedUserName: {
1787
+ type: "string",
1788
+ required: false
1789
+ },
1790
+ /**
1791
+ * Denormalized display name of the linked Role, captured at row
1792
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1793
+ * adjacency-list user-projection SK (pattern #5 —
1794
+ * `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#…`) can be composed from
1795
+ * a top-level field. Optional on the schema so pre-TR-024 rows do not
1796
+ * break; the operations-layer multi-write helper (#1010) makes the
1797
+ * field load-bearing at write time per TR-024 rule 2 (write-time
1798
+ * source = canonical Role.displayName).
1799
+ * @see TR-024 — Denormalized display-name attributes
1800
+ */
1801
+ denormalizedRoleName: {
1802
+ type: "string",
1803
+ required: false
1804
+ }
1805
+ },
1806
+ indexes: {
1807
+ /** Base table: PK = TID#<tenantId>#ROLEASSIGNMENT#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
1808
+ record: {
1809
+ pk: {
1810
+ field: "PK",
1811
+ composite: ["tenantId", "id"],
1812
+ template: "TID#${tenantId}#ROLEASSIGNMENT#ID#${id}"
1813
+ },
1814
+ sk: {
1815
+ field: "SK",
1816
+ composite: ["sk"],
1817
+ template: "${sk}"
1125
1818
  }
1126
1819
  },
1127
1820
  /**
1128
1821
  * GSI1 — Unified Sharded List per ADR-011: list all RoleAssignments for a tenant across the
1129
1822
  * four shards. Tenant-scoped only, so `WID#-` is a sentinel.
1130
- * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
1131
- * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
1132
- * normalized label and ISO-8601 `T`/`Z`.
1823
+ * SK is derived via `roleAssignmentGsi1skAttribute` — composes the
1824
+ * discriminator-first `<roleId>#<normalizedUserName>#<id>` shape per
1825
+ * ADR-018 pattern #8 (users with a specific role in a tenant, sorted
1826
+ * by user name); falls back to `<lastUpdated>#<id>` when either
1827
+ * component is missing. `casing: "none"` preserves the normalized
1828
+ * label and ISO-8601 `T`/`Z`.
1133
1829
  */
1134
1830
  gsi1: {
1135
1831
  index: "GSI1",
@@ -1148,9 +1844,285 @@ var RoleAssignmentEntity = new import_electrodb4.Entity({
1148
1844
  }
1149
1845
  });
1150
1846
 
1847
+ // src/data/dynamo/entities/control/roleassignment-user-projection-entity.ts
1848
+ var import_electrodb9 = require("electrodb");
1849
+ var RoleAssignmentUserProjectionEntity = new import_electrodb9.Entity({
1850
+ model: {
1851
+ entity: "roleAssignmentUserProjection",
1852
+ service: "control",
1853
+ version: "01"
1854
+ },
1855
+ attributes: {
1856
+ /**
1857
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
1858
+ * base-table PK. Always required — the projection has no meaning
1859
+ * outside a user partition.
1860
+ */
1861
+ userId: {
1862
+ type: "string",
1863
+ required: true
1864
+ },
1865
+ /**
1866
+ * Pre-composed sort key — built by the operations-layer projection
1867
+ * writer via `buildRoleAssignmentUserProjectionSk*` helpers. The
1868
+ * entity stores the value verbatim so the SK grammar (tenant-lane
1869
+ * vs workspace-lane) is owned by the operations layer, not
1870
+ * duplicated here.
1871
+ */
1872
+ sk: {
1873
+ type: "string",
1874
+ required: true
1875
+ },
1876
+ /** Tenant in which the role assignment applies. Always required. */
1877
+ tenantId: {
1878
+ type: "string",
1879
+ required: true
1880
+ },
1881
+ /**
1882
+ * Workspace the role assignment scopes to. Present iff the
1883
+ * projection row is the workspace-level sub-lane; absent for
1884
+ * tenant-level sub-lane rows.
1885
+ */
1886
+ workspaceId: {
1887
+ type: "string",
1888
+ required: false
1889
+ },
1890
+ /**
1891
+ * Role the assignment grants. Stored as a discriminating field so
1892
+ * `Query(PK = USER#ID#<userId>, SK begins_with 'ROLEASSIGNMENT#…')`
1893
+ * results carry the role id without a hop to the canonical row.
1894
+ */
1895
+ roleId: {
1896
+ type: "string",
1897
+ required: true
1898
+ },
1899
+ /**
1900
+ * RoleAssignment canonical-record id. Stored as a discriminating
1901
+ * field so consumers can hydrate the canonical row via
1902
+ * `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
1903
+ * when the projection's `summary` is insufficient.
1904
+ */
1905
+ roleAssignmentId: {
1906
+ type: "string",
1907
+ required: true
1908
+ },
1909
+ /**
1910
+ * Summary projection (key display fields as JSON string: id,
1911
+ * displayName, status) — mirrored from the canonical RoleAssignment
1912
+ * row so user-partition queries do not need a BatchGet hop.
1913
+ */
1914
+ summary: {
1915
+ type: "string",
1916
+ required: true
1917
+ },
1918
+ /** Version id mirrored from the canonical RoleAssignment row. */
1919
+ vid: {
1920
+ type: "string",
1921
+ required: true
1922
+ },
1923
+ /** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
1924
+ lastUpdated: {
1925
+ type: "string",
1926
+ required: true
1927
+ },
1928
+ /**
1929
+ * Denormalized Tenant display name — mirrored from the canonical
1930
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
1931
+ * Optional on the schema because pre-TR-024 rows may not carry a
1932
+ * display name; the operations layer falls back gracefully when
1933
+ * missing.
1934
+ */
1935
+ denormalizedTenantName: {
1936
+ type: "string",
1937
+ required: false
1938
+ },
1939
+ /**
1940
+ * Denormalized User display name — mirrored from the canonical
1941
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
1942
+ * Carried on the projection so consumers can render the user's
1943
+ * display name without a hop to the User record.
1944
+ */
1945
+ denormalizedUserName: {
1946
+ type: "string",
1947
+ required: false
1948
+ },
1949
+ /**
1950
+ * Denormalized Role display name — required to compose the SK's
1951
+ * `<normalizedRoleName>` segment. Optional on the schema (pre-TR-024
1952
+ * rows fall back to a sentinel) but expected to be present at write
1953
+ * time per TR-024 rule 2 (write-time source =
1954
+ * canonical Role.displayName).
1955
+ */
1956
+ denormalizedRoleName: {
1957
+ type: "string",
1958
+ required: false
1959
+ }
1960
+ },
1961
+ indexes: {
1962
+ /**
1963
+ * Base table: PK = USER#ID#<userId>, SK = operation-supplied. Both
1964
+ * sub-lanes (tenant-level and workspace-level) use this same index —
1965
+ * the SK string encodes the lane discriminator
1966
+ * (`ROLEASSIGNMENT#TENANT#…` vs `ROLEASSIGNMENT#WORKSPACE#…`) so a
1967
+ * single `Query(PK = USER#ID#<userId>, SK begins_with
1968
+ * 'ROLEASSIGNMENT#')` returns both lanes interleaved.
1969
+ */
1970
+ record: {
1971
+ pk: {
1972
+ field: "PK",
1973
+ composite: ["userId"],
1974
+ template: "USER#ID#${userId}"
1975
+ },
1976
+ sk: {
1977
+ field: "SK",
1978
+ casing: "none",
1979
+ composite: ["sk"],
1980
+ template: "${sk}"
1981
+ }
1982
+ }
1983
+ }
1984
+ });
1985
+
1986
+ // src/data/dynamo/entities/control/roleassignment-workspace-projection-entity.ts
1987
+ var import_electrodb10 = require("electrodb");
1988
+ var RoleAssignmentWorkspaceProjectionEntity = new import_electrodb10.Entity({
1989
+ model: {
1990
+ entity: "roleAssignmentWorkspaceProjection",
1991
+ service: "control",
1992
+ version: "01"
1993
+ },
1994
+ attributes: {
1995
+ /**
1996
+ * Tenant the workspace belongs to. Renders as the leading segment
1997
+ * of the base-table PK. Always required — the workspace partition
1998
+ * is tenant-scoped per ADR-011.
1999
+ */
2000
+ tenantId: {
2001
+ type: "string",
2002
+ required: true
2003
+ },
2004
+ /**
2005
+ * Workspace partition discriminator. Renders as the trailing
2006
+ * segment of the base-table PK
2007
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
2008
+ * the projection has no meaning outside a workspace partition.
2009
+ */
2010
+ workspaceId: {
2011
+ type: "string",
2012
+ required: true
2013
+ },
2014
+ /**
2015
+ * Pre-composed sort key — built by the operations-layer projection
2016
+ * writer via `buildRoleAssignmentWorkspaceProjectionSk`. The entity
2017
+ * stores the value verbatim so the SK grammar (pattern #9) is
2018
+ * owned by the operations layer, not duplicated here.
2019
+ */
2020
+ sk: {
2021
+ type: "string",
2022
+ required: true
2023
+ },
2024
+ /**
2025
+ * User the role assignment grants the role to. Stored as a
2026
+ * discriminating field so consumers can hydrate the canonical User
2027
+ * row via `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
2028
+ * projection's `summary` is insufficient.
2029
+ */
2030
+ userId: {
2031
+ type: "string",
2032
+ required: true
2033
+ },
2034
+ /**
2035
+ * Role the assignment grants. Stored as a discriminating field —
2036
+ * also rendered into the SK as the discriminator-first segment so
2037
+ * `begins_with('ROLEASSIGNMENT#<roleId>#')` filters one role.
2038
+ */
2039
+ roleId: {
2040
+ type: "string",
2041
+ required: true
2042
+ },
2043
+ /**
2044
+ * RoleAssignment canonical-record id. Stored as a discriminating
2045
+ * field so consumers can hydrate the canonical row via
2046
+ * `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
2047
+ * when the projection's `summary` is insufficient.
2048
+ */
2049
+ roleAssignmentId: {
2050
+ type: "string",
2051
+ required: true
2052
+ },
2053
+ /**
2054
+ * Summary projection (key display fields as JSON string: id,
2055
+ * displayName, status) — mirrored from the canonical RoleAssignment
2056
+ * row so workspace-partition queries do not need a BatchGet hop.
2057
+ */
2058
+ summary: {
2059
+ type: "string",
2060
+ required: true
2061
+ },
2062
+ /** Version id mirrored from the canonical RoleAssignment row. */
2063
+ vid: {
2064
+ type: "string",
2065
+ required: true
2066
+ },
2067
+ /** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
2068
+ lastUpdated: {
2069
+ type: "string",
2070
+ required: true
2071
+ },
2072
+ /**
2073
+ * Denormalized User display name — required to compose the
2074
+ * pattern-#9 SK (`ROLEASSIGNMENT#<roleId>#<normalizedUserName>#…`).
2075
+ * Optional on the schema because pre-TR-024 rows may not carry a
2076
+ * display name; the operations layer falls back to a sentinel when
2077
+ * missing so the SK still has a valid shape. The TR-023 rename-
2078
+ * cascade pipeline rewrites the SK on a User rename.
2079
+ */
2080
+ denormalizedUserName: {
2081
+ type: "string",
2082
+ required: false
2083
+ },
2084
+ /**
2085
+ * Denormalized Role display name — mirrored from the canonical
2086
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
2087
+ * Carried on the projection so consumers can render the role's
2088
+ * display name without a hop to the Role record. Not part of the
2089
+ * SK (pattern #9 sorts on `<normalizedUserName>`, not role name) —
2090
+ * a Role rename does NOT rewrite this SK.
2091
+ */
2092
+ denormalizedRoleName: {
2093
+ type: "string",
2094
+ required: false
2095
+ }
2096
+ },
2097
+ indexes: {
2098
+ /**
2099
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
2100
+ * SK = operation-supplied. Pattern #9 uses this index — the SK
2101
+ * encodes the entity-type prefix and discriminator-first roleId
2102
+ * (`ROLEASSIGNMENT#<roleId>#…`) so
2103
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'ROLEASSIGNMENT#<roleId>#')`
2104
+ * returns every user-assignment for that role in the workspace, sorted
2105
+ * by normalized user name.
2106
+ */
2107
+ record: {
2108
+ pk: {
2109
+ field: "PK",
2110
+ composite: ["tenantId", "workspaceId"],
2111
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
2112
+ },
2113
+ sk: {
2114
+ field: "SK",
2115
+ casing: "none",
2116
+ composite: ["sk"],
2117
+ template: "${sk}"
2118
+ }
2119
+ }
2120
+ }
2121
+ });
2122
+
1151
2123
  // src/data/dynamo/entities/control/tenant-entity.ts
1152
- var import_electrodb5 = require("electrodb");
1153
- var TenantEntity = new import_electrodb5.Entity({
2124
+ var import_electrodb11 = require("electrodb");
2125
+ var TenantEntity = new import_electrodb11.Entity({
1154
2126
  model: {
1155
2127
  entity: "tenant",
1156
2128
  service: "control",
@@ -1250,8 +2222,8 @@ var TenantEntity = new import_electrodb5.Entity({
1250
2222
  });
1251
2223
 
1252
2224
  // src/data/dynamo/entities/control/user-entity.ts
1253
- var import_electrodb6 = require("electrodb");
1254
- var UserEntity = new import_electrodb6.Entity({
2225
+ var import_electrodb12 = require("electrodb");
2226
+ var UserEntity = new import_electrodb12.Entity({
1255
2227
  model: {
1256
2228
  entity: "user",
1257
2229
  service: "control",
@@ -1306,6 +2278,28 @@ var UserEntity = new import_electrodb6.Entity({
1306
2278
  type: "boolean",
1307
2279
  required: false
1308
2280
  },
2281
+ /**
2282
+ * TR-022 / ADR-018 lifecycle state for the cascade pipeline.
2283
+ *
2284
+ * - `active` (or undefined) — normal, readable state.
2285
+ * - `deleting` — intermediate state set synchronously by the
2286
+ * hard-delete API entry point. The owning-delete cascade state
2287
+ * machine fans out from this transition (DynamoDB stream →
2288
+ * `control-plane.owning-delete.v1` → Step Functions). Readers MUST
2289
+ * short-circuit on `deleting` so partial cascades stay invisible.
2290
+ * - `deleted-failed` — terminal failure state set by the cascade
2291
+ * finalize Lambda when the cascade run fails irrecoverably.
2292
+ * Operators recover by re-running the cascade or by direct
2293
+ * intervention.
2294
+ *
2295
+ * The cascade finalize step deletes the canonical record conditional
2296
+ * on `lifecycleState = "deleting"`; on replay the conditional check
2297
+ * fails and the finalize step treats that as a no-op success.
2298
+ */
2299
+ lifecycleState: {
2300
+ type: ["active", "deleting", "deleted-failed"],
2301
+ required: false
2302
+ },
1309
2303
  bundleId: {
1310
2304
  type: "string",
1311
2305
  required: false
@@ -1375,8 +2369,8 @@ var UserEntity = new import_electrodb6.Entity({
1375
2369
  });
1376
2370
 
1377
2371
  // src/data/dynamo/entities/control/workspace-entity.ts
1378
- var import_electrodb7 = require("electrodb");
1379
- var WorkspaceEntity = new import_electrodb7.Entity({
2372
+ var import_electrodb13 = require("electrodb");
2373
+ var WorkspaceEntity = new import_electrodb13.Entity({
1380
2374
  model: {
1381
2375
  entity: "workspace",
1382
2376
  service: "control",
@@ -1428,6 +2422,28 @@ var WorkspaceEntity = new import_electrodb7.Entity({
1428
2422
  type: "boolean",
1429
2423
  required: false
1430
2424
  },
2425
+ /**
2426
+ * TR-022 / ADR-018 lifecycle state for the cascade pipeline.
2427
+ *
2428
+ * - `active` (or undefined) — normal, readable state.
2429
+ * - `deleting` — intermediate state set synchronously by the
2430
+ * hard-delete API entry point. The owning-delete cascade state
2431
+ * machine fans out from this transition (DynamoDB stream →
2432
+ * `control-plane.owning-delete.v1` → Step Functions). Readers MUST
2433
+ * short-circuit on `deleting` so partial cascades stay invisible.
2434
+ * - `deleted-failed` — terminal failure state set by the cascade
2435
+ * finalize Lambda when the cascade run fails irrecoverably.
2436
+ * Operators recover by re-running the cascade or by direct
2437
+ * intervention.
2438
+ *
2439
+ * The cascade finalize step deletes the canonical record conditional
2440
+ * on `lifecycleState = "deleting"`; on replay the conditional check
2441
+ * fails and the finalize step treats that as a no-op success.
2442
+ */
2443
+ lifecycleState: {
2444
+ type: ["active", "deleting", "deleted-failed"],
2445
+ required: false
2446
+ },
1431
2447
  bundleId: {
1432
2448
  type: "string",
1433
2449
  required: false
@@ -1478,28 +2494,36 @@ var WorkspaceEntity = new import_electrodb7.Entity({
1478
2494
  // src/data/dynamo/dynamo-control-service.ts
1479
2495
  var controlPlaneEntities = {
1480
2496
  configuration: ConfigurationEntity,
2497
+ configurationUserProjection: ConfigurationUserProjectionEntity,
2498
+ configurationWorkspaceProjection: ConfigurationWorkspaceProjectionEntity,
1481
2499
  membership: MembershipEntity,
2500
+ membershipUserProjection: MembershipUserProjectionEntity,
2501
+ membershipWorkspaceProjection: MembershipWorkspaceProjectionEntity,
1482
2502
  role: RoleEntity,
1483
2503
  roleAssignment: RoleAssignmentEntity,
2504
+ roleAssignmentUserProjection: RoleAssignmentUserProjectionEntity,
2505
+ roleAssignmentWorkspaceProjection: RoleAssignmentWorkspaceProjectionEntity,
1484
2506
  tenant: TenantEntity,
1485
2507
  user: UserEntity,
1486
2508
  workspace: WorkspaceEntity
1487
2509
  };
1488
- var controlPlaneService = new import_electrodb8.Service(controlPlaneEntities, {
2510
+ var controlPlaneService = new import_electrodb14.Service(controlPlaneEntities, {
1489
2511
  table: defaultTableName,
1490
2512
  client: dynamoClient
1491
2513
  });
1492
2514
  var DynamoControlService = {
1493
- entities: controlPlaneService.entities
2515
+ entities: controlPlaneService.entities,
2516
+ transaction: controlPlaneService.transaction
1494
2517
  };
1495
2518
  function getDynamoControlService(tableName) {
1496
2519
  const resolved = tableName ?? defaultTableName;
1497
- const service = new import_electrodb8.Service(controlPlaneEntities, {
2520
+ const service = new import_electrodb14.Service(controlPlaneEntities, {
1498
2521
  table: resolved,
1499
2522
  client: dynamoClient
1500
2523
  });
1501
2524
  return {
1502
- entities: service.entities
2525
+ entities: service.entities,
2526
+ transaction: service.transaction
1503
2527
  };
1504
2528
  }
1505
2529