@openhi/constructs 0.0.159 → 0.0.161

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 (129) hide show
  1. package/lib/{chunk-HQ67J7BP.mjs → chunk-5S6VFBLT.mjs} +12 -70
  2. package/lib/chunk-5S6VFBLT.mjs.map +1 -0
  3. package/lib/{chunk-MVQWAIMC.mjs → chunk-6BB4CRSS.mjs} +3 -312
  4. package/lib/chunk-6BB4CRSS.mjs.map +1 -0
  5. package/lib/{chunk-WPCBVDFZ.mjs → chunk-76UM2LQ5.mjs} +2 -2
  6. package/lib/chunk-7TRO2STL.mjs +4616 -0
  7. package/lib/chunk-7TRO2STL.mjs.map +1 -0
  8. package/lib/chunk-BUAYVN3C.mjs +87 -0
  9. package/lib/chunk-BUAYVN3C.mjs.map +1 -0
  10. package/lib/{chunk-23PUSHBV.mjs → chunk-D2Y6DDOC.mjs} +2 -2
  11. package/lib/chunk-DWSWCUZR.mjs +123 -0
  12. package/lib/chunk-DWSWCUZR.mjs.map +1 -0
  13. package/lib/{chunk-VZCPGQXA.mjs → chunk-EUIP2U5F.mjs} +69 -1
  14. package/lib/{chunk-VZCPGQXA.mjs.map → chunk-EUIP2U5F.mjs.map} +1 -1
  15. package/lib/chunk-GJTPXJKD.mjs +46 -0
  16. package/lib/chunk-GJTPXJKD.mjs.map +1 -0
  17. package/lib/chunk-I6LUPJUY.mjs +61 -0
  18. package/lib/chunk-I6LUPJUY.mjs.map +1 -0
  19. package/lib/{chunk-KR2Y2CVQ.mjs → chunk-KA3OMP3X.mjs} +2 -2
  20. package/lib/{chunk-ZM4GDHHC.mjs → chunk-KMEWULMX.mjs} +51 -3
  21. package/lib/chunk-KMEWULMX.mjs.map +1 -0
  22. package/lib/chunk-LKKLO66E.mjs +25 -0
  23. package/lib/chunk-LKKLO66E.mjs.map +1 -0
  24. package/lib/{chunk-CFJDATDK.mjs → chunk-MLFMW5IF.mjs} +43 -9
  25. package/lib/chunk-MLFMW5IF.mjs.map +1 -0
  26. package/lib/chunk-O5VQWB6U.mjs +315 -0
  27. package/lib/chunk-O5VQWB6U.mjs.map +1 -0
  28. package/lib/{chunk-7BQHLC7U.mjs → chunk-P3CTZWC2.mjs} +8 -40
  29. package/lib/chunk-P3CTZWC2.mjs.map +1 -0
  30. package/lib/chunk-P3NFCKTZ.mjs +502 -0
  31. package/lib/chunk-P3NFCKTZ.mjs.map +1 -0
  32. package/lib/{chunk-M7Y3BOQW.mjs → chunk-Q3MKITPY.mjs} +5 -5
  33. package/lib/chunk-Q64MOYJ7.mjs +218 -0
  34. package/lib/chunk-Q64MOYJ7.mjs.map +1 -0
  35. package/lib/chunk-RQKJNMX5.mjs +89 -0
  36. package/lib/chunk-RQKJNMX5.mjs.map +1 -0
  37. package/lib/{chunk-ZWSGM6PZ.mjs → chunk-SD7J3N3C.mjs} +2 -2
  38. package/lib/{chunk-7RZHFI77.mjs → chunk-VESULYQQ.mjs} +2 -2
  39. package/lib/{chunk-AOSEKL7U.mjs → chunk-WOTU36P3.mjs} +6 -103
  40. package/lib/chunk-WOTU36P3.mjs.map +1 -0
  41. package/lib/{chunk-X5E4YJGZ.mjs → chunk-YPTJJ35S.mjs} +2 -2
  42. package/lib/counter-apply-operation-DZM3MIDm.d.mts +63 -0
  43. package/lib/counter-apply-operation-DZM3MIDm.d.ts +63 -0
  44. package/lib/counter-maintenance.handler.d.mts +38 -0
  45. package/lib/counter-maintenance.handler.d.ts +38 -0
  46. package/lib/counter-maintenance.handler.js +2885 -0
  47. package/lib/counter-maintenance.handler.js.map +1 -0
  48. package/lib/counter-maintenance.handler.mjs +180 -0
  49. package/lib/counter-maintenance.handler.mjs.map +1 -0
  50. package/lib/counter-reconciliation.handler.d.mts +116 -0
  51. package/lib/counter-reconciliation.handler.d.ts +116 -0
  52. package/lib/counter-reconciliation.handler.js +3324 -0
  53. package/lib/counter-reconciliation.handler.js.map +1 -0
  54. package/lib/counter-reconciliation.handler.mjs +295 -0
  55. package/lib/counter-reconciliation.handler.mjs.map +1 -0
  56. package/lib/data-store-postgres-replication.handler.js +50 -2
  57. package/lib/data-store-postgres-replication.handler.js.map +1 -1
  58. package/lib/data-store-postgres-replication.handler.mjs +2 -2
  59. package/lib/delete-chunk.handler.js +118 -2
  60. package/lib/delete-chunk.handler.js.map +1 -1
  61. package/lib/delete-chunk.handler.mjs +3 -3
  62. package/lib/{events-DTgo2dcW.d.mts → events-TG654e7L.d.mts} +68 -19
  63. package/lib/{events-DTgo2dcW.d.ts → events-TG654e7L.d.ts} +68 -19
  64. package/lib/finalize.handler.js +50 -2
  65. package/lib/finalize.handler.js.map +1 -1
  66. package/lib/finalize.handler.mjs +4 -4
  67. package/lib/firehose-archive-transform.handler.js +50 -2
  68. package/lib/firehose-archive-transform.handler.js.map +1 -1
  69. package/lib/firehose-archive-transform.handler.mjs +2 -2
  70. package/lib/index.d.mts +1283 -4
  71. package/lib/index.d.ts +1389 -24
  72. package/lib/index.js +4113 -320
  73. package/lib/index.js.map +1 -1
  74. package/lib/index.mjs +602 -195
  75. package/lib/index.mjs.map +1 -1
  76. package/lib/list-chunks.handler.js +118 -2
  77. package/lib/list-chunks.handler.js.map +1 -1
  78. package/lib/list-chunks.handler.mjs +3 -3
  79. package/lib/platform-deploy-bridge.handler.js +50 -2
  80. package/lib/platform-deploy-bridge.handler.js.map +1 -1
  81. package/lib/platform-deploy-bridge.handler.mjs +1 -1
  82. package/lib/pre-token-generation.handler.js +68 -0
  83. package/lib/pre-token-generation.handler.js.map +1 -1
  84. package/lib/pre-token-generation.handler.mjs +9 -5
  85. package/lib/pre-token-generation.handler.mjs.map +1 -1
  86. package/lib/provision-default-workspace.handler.js +887 -4
  87. package/lib/provision-default-workspace.handler.js.map +1 -1
  88. package/lib/provision-default-workspace.handler.mjs +14 -9
  89. package/lib/provision-default-workspace.handler.mjs.map +1 -1
  90. package/lib/rename-finalize.handler.js +50 -2
  91. package/lib/rename-finalize.handler.js.map +1 -1
  92. package/lib/rename-finalize.handler.mjs +2 -2
  93. package/lib/rename-list-targets.handler.js +118 -2
  94. package/lib/rename-list-targets.handler.js.map +1 -1
  95. package/lib/rename-list-targets.handler.mjs +11 -9
  96. package/lib/rename-list-targets.handler.mjs.map +1 -1
  97. package/lib/rename-rewrite-chunk.handler.js +68 -0
  98. package/lib/rename-rewrite-chunk.handler.js.map +1 -1
  99. package/lib/rename-rewrite-chunk.handler.mjs +2 -2
  100. package/lib/rest-api-lambda.handler.js +1454 -251
  101. package/lib/rest-api-lambda.handler.js.map +1 -1
  102. package/lib/rest-api-lambda.handler.mjs +673 -821
  103. package/lib/rest-api-lambda.handler.mjs.map +1 -1
  104. package/lib/seed-demo-data.handler.d.mts +1 -1
  105. package/lib/seed-demo-data.handler.d.ts +1 -1
  106. package/lib/seed-demo-data.handler.js +4004 -201
  107. package/lib/seed-demo-data.handler.js.map +1 -1
  108. package/lib/seed-demo-data.handler.mjs +10 -7
  109. package/lib/seed-system-data.handler.js +118 -2
  110. package/lib/seed-system-data.handler.js.map +1 -1
  111. package/lib/seed-system-data.handler.mjs +5 -5
  112. package/package.json +1 -1
  113. package/lib/chunk-7BQHLC7U.mjs.map +0 -1
  114. package/lib/chunk-AOSEKL7U.mjs.map +0 -1
  115. package/lib/chunk-BQMJSDOD.mjs +0 -1136
  116. package/lib/chunk-BQMJSDOD.mjs.map +0 -1
  117. package/lib/chunk-CFJDATDK.mjs.map +0 -1
  118. package/lib/chunk-E6MCKJVS.mjs +0 -212
  119. package/lib/chunk-E6MCKJVS.mjs.map +0 -1
  120. package/lib/chunk-HQ67J7BP.mjs.map +0 -1
  121. package/lib/chunk-MVQWAIMC.mjs.map +0 -1
  122. package/lib/chunk-ZM4GDHHC.mjs.map +0 -1
  123. /package/lib/{chunk-WPCBVDFZ.mjs.map → chunk-76UM2LQ5.mjs.map} +0 -0
  124. /package/lib/{chunk-23PUSHBV.mjs.map → chunk-D2Y6DDOC.mjs.map} +0 -0
  125. /package/lib/{chunk-KR2Y2CVQ.mjs.map → chunk-KA3OMP3X.mjs.map} +0 -0
  126. /package/lib/{chunk-M7Y3BOQW.mjs.map → chunk-Q3MKITPY.mjs.map} +0 -0
  127. /package/lib/{chunk-ZWSGM6PZ.mjs.map → chunk-SD7J3N3C.mjs.map} +0 -0
  128. /package/lib/{chunk-7RZHFI77.mjs.map → chunk-VESULYQQ.mjs.map} +0 -0
  129. /package/lib/{chunk-X5E4YJGZ.mjs.map → chunk-YPTJJ35S.mjs.map} +0 -0
@@ -1,24 +1,26 @@
1
1
  import {
2
2
  chunkRenameCascadeTargets
3
- } from "./chunk-X5E4YJGZ.mjs";
3
+ } from "./chunk-YPTJJ35S.mjs";
4
4
  import {
5
- require_lib
6
- } from "./chunk-ZM4GDHHC.mjs";
7
- import {
8
- buildMembershipUserProjectionSkTenantLane,
9
- buildMembershipUserProjectionSkWorkspaceLane,
10
5
  buildMembershipWorkspaceProjectionSk,
11
6
  buildRoleAssignmentUserProjectionSkTenantLane,
12
7
  buildRoleAssignmentUserProjectionSkWorkspaceLane,
13
- buildRoleAssignmentWorkspaceProjectionSk,
8
+ buildRoleAssignmentWorkspaceProjectionSk
9
+ } from "./chunk-5S6VFBLT.mjs";
10
+ import {
11
+ buildMembershipUserProjectionSkTenantLane,
12
+ buildMembershipUserProjectionSkWorkspaceLane,
14
13
  extractReferenceSlug
15
- } from "./chunk-HQ67J7BP.mjs";
14
+ } from "./chunk-I6LUPJUY.mjs";
16
15
  import "./chunk-QJDHVMKT.mjs";
17
16
  import "./chunk-FYHBHHWK.mjs";
18
17
  import {
19
18
  getDynamoControlService
20
- } from "./chunk-VZCPGQXA.mjs";
19
+ } from "./chunk-EUIP2U5F.mjs";
21
20
  import "./chunk-TRY7JGWO.mjs";
21
+ import {
22
+ require_lib
23
+ } from "./chunk-KMEWULMX.mjs";
22
24
  import {
23
25
  __toESM
24
26
  } from "./chunk-LZOMFHX3.mjs";
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/workflows/control-plane/rename-cascade/rename-list-targets.handler.ts","../src/data/operations/control/rename-cascade/rename-cascade-list-targets-operation.ts"],"sourcesContent":["/**\n * Cascade Step Functions handler that queries one page of projection\n * rows affected by a Tenant / User / Role rename and bundles them into\n * \\<=50-target chunks for the downstream Distributed Map state.\n *\n * One invocation per outer-loop iteration:\n *\n * 1. Calls `listRenameCascadeTargetsOperation` with the per-stream\n * cursor map from the prior iteration (`{}` on the first call).\n * 2. Splits the merged page into chunks via `chunkRenameCascadeTargets`.\n * 3. Stamps each chunk with a deterministic `chunkToken` so a replayed\n * Map iteration lands idempotently via `executeMultiWrite`'s\n * `ClientRequestToken` forwarding.\n * 4. Returns the chunks, the new cursors, and the cumulative metrics\n * so the state machine's outer `Choice` knows when to stop.\n *\n * The handler itself NEVER touches the canonical Tenant / User / Role\n * record; the cascade is a consumer that only rewrites projection rows.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport type {\n RenameCascadeChunkInput,\n RenameCascadeListInput,\n RenameCascadeListOutput,\n} from \"./events\";\nimport {\n type RenameCascadeCursorMap,\n listRenameCascadeTargetsOperation,\n} from \"../../../data/operations/control/rename-cascade/rename-cascade-list-targets-operation\";\nimport { chunkRenameCascadeTargets } from \"../../../data/operations/control/rename-cascade/rename-cascade-rewrite-chunk-operation\";\n\nexport const handler = async (\n input: RenameCascadeListInput,\n): Promise<RenameCascadeListOutput> => {\n const cursors: RenameCascadeCursorMap = {};\n if (input.cursors) {\n for (const [key, value] of Object.entries(input.cursors)) {\n cursors[key] = value;\n }\n }\n\n const page = await listRenameCascadeTargetsOperation({\n entityType: input.entityType,\n entityId: input.entityId,\n tenantId: input.tenantId,\n oldName: input.oldName,\n newName: input.newName,\n oldNormalizedName: input.oldNormalizedName,\n newNormalizedName: input.newNormalizedName,\n cursors,\n });\n\n const chunks: Array<RenameCascadeChunkInput> = chunkRenameCascadeTargets(\n page.targets,\n ).map((targets) => ({\n entityType: input.entityType,\n entityId: input.entityId,\n tenantId: input.tenantId,\n targets,\n chunkToken: randomUUID(),\n }));\n\n const priorRewritten = input.itemsRewritten ?? 0;\n const priorChunks = input.chunkCount ?? 0;\n const itemsRewritten = priorRewritten + page.targets.length;\n const chunkCount = priorChunks + chunks.length;\n\n return {\n entityType: input.entityType,\n entityId: input.entityId,\n tenantId: input.tenantId,\n oldName: input.oldName,\n newName: input.newName,\n oldNormalizedName: input.oldNormalizedName,\n newNormalizedName: input.newNormalizedName,\n cursors: page.cursors,\n chunks,\n exhausted: page.exhausted,\n itemsRewritten,\n chunkCount,\n };\n};\n","/**\n * Enumerate projection rows affected by a Tenant / User / Role rename\n * for the TR-023 rename cascade.\n *\n * One page per call; the cascade state machine outer loop walks the\n * returned `cursors` map back into this operation until every per-entity\n * stream returns `null`. Each emitted row carries:\n *\n * - the projection-entity name (so the rewrite-chunk operation can map\n * it to the correct ElectroDB entity in `executeMultiWrite`),\n * - the **existing** composite key (used for the `delete` triple in the\n * transact-write pair),\n * - the **new** composite key (used for the `put` triple — same row\n * identity but a rewritten SK when the SK encodes the renamed\n * normalized name), and\n * - the row's existing attributes (carried verbatim into the `put` so\n * `summary`, `vid`, `lastUpdated`, etc. are preserved across the\n * rewrite), with the renamed `denormalized<CarrierEntity>Name`\n * replaced by the new display name.\n *\n * Per-entityType query plan (per the ADR-018 implementation guide § 5):\n *\n * - **User rename**: under `PK = USER#ID#<userId>` — Membership user-\n * projection rows (patterns #3 + #4) and RoleAssignment user-projection\n * rows (pattern #5). Workspace-side projection rows\n * (membershipWorkspaceProjection #2 + roleAssignmentWorkspaceProjection\n * #9) encode `<normalizedUserName>` in their SK; this operation\n * discovers the affected workspaces from the user's pattern-#4\n * memberships and queries each workspace partition for them.\n * - **Role rename**: under every affected user partition — RoleAssignment\n * user-projection rows (pattern #5) sort on `<normalizedRoleName>` and\n * need a SK rewrite. RoleAssignment canonical (pattern #8) and\n * workspace-projection (pattern #9) sort on raw `<roleId>` so only the\n * denormalized attr changes (no SK rewrite). The affected user-ids\n * are discovered via the canonical RoleAssignment GSI1 (`<roleId>#`\n * prefix).\n * - **Tenant rename**: only `denormalizedTenantName` updates — SKs do\n * not carry tenant-name; the row identity is preserved. Affected user-\n * ids are discovered via the canonical Membership GSI1 page.\n *\n * For #1023 the User-rename path is implemented in full; the Tenant /\n * Role discovery hooks are scaffolded with the right query shape and\n * cursor map but only walk one canonical discovery batch per call (the\n * cascade outer loop pages through them). See § 5 of the implementation\n * guide for the full matrix.\n *\n * @see .state/adr-018-implementation-guide.md § 5 (TR-023 Rename-Cascade Consumer Contract)\n * @see .claude/rules/data-layer-layout.md\n */\n\nimport {\n RENAMABLE_ENTITY_TYPE,\n type RenamableEntityType,\n} from \"@openhi/workflows\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport {\n buildMembershipUserProjectionSkTenantLane,\n buildMembershipUserProjectionSkWorkspaceLane,\n extractReferenceSlug,\n} from \"../membership/membership-user-projection\";\nimport { buildMembershipWorkspaceProjectionSk } from \"../membership/membership-workspace-projection\";\nimport {\n buildRoleAssignmentUserProjectionSkTenantLane,\n buildRoleAssignmentUserProjectionSkWorkspaceLane,\n} from \"../roleassignment/roleassignment-user-projection\";\nimport { buildRoleAssignmentWorkspaceProjectionSk } from \"../roleassignment/roleassignment-workspace-projection\";\n\n/**\n * Projection-entity name keys this operation may emit. Each key maps to\n * an entity in the control-plane service; the rewrite-chunk consumer\n * forwards it to `executeMultiWrite` as the `entity` field on a triple.\n */\nexport const RENAME_CASCADE_PROJECTION_ENTITY = {\n MembershipUserProjection: \"membershipUserProjection\",\n MembershipWorkspaceProjection: \"membershipWorkspaceProjection\",\n RoleAssignmentUserProjection: \"roleAssignmentUserProjection\",\n RoleAssignmentWorkspaceProjection: \"roleAssignmentWorkspaceProjection\",\n} as const;\nexport type RenameCascadeProjectionEntity =\n (typeof RENAME_CASCADE_PROJECTION_ENTITY)[keyof typeof RENAME_CASCADE_PROJECTION_ENTITY];\n\n/**\n * One row to rewrite — the cascade rewrite-chunk operation turns each\n * entry into a `delete oldKey` + `put newPayload` transact-write pair.\n *\n * `oldKey` and `newKey` differ only in the SK segment when the SK\n * encodes a normalized form of the renamed name. For Tenant rename and\n * for SK-stable RoleAssignment projections (canonical pattern #8 and\n * workspace pattern #9 under a Role rename), `oldKey === newKey` and\n * the rewrite collapses to a single `put` overwrite.\n */\nexport interface RenameCascadeRewriteTarget {\n readonly entity: RenameCascadeProjectionEntity;\n /** Composite key payload for the existing row. */\n readonly oldKey: Record<string, string>;\n /** Composite key payload for the rewritten row. */\n readonly newKey: Record<string, string>;\n /**\n * Full row payload to write at `newKey` — carries the existing\n * `summary`, `vid`, `lastUpdated`, and discriminating fields, with\n * the renamed `denormalized<CarrierEntity>Name` swapped to the new\n * display name.\n */\n readonly newItem: Record<string, unknown>;\n /**\n * `true` when `oldKey` and `newKey` differ — the rewrite must atomic\n * delete the old row and put the new row in the same transaction.\n * `false` when only the denormalized attr changes — a single `put`\n * overwrite is sufficient.\n */\n readonly skRewriteRequired: boolean;\n}\n\n/** Per-stream cursor — `null` marks a stream as exhausted. */\nexport type RenameCascadeCursorMap = Record<string, string | null>;\n\n/** Inputs accepted by {@link listRenameCascadeTargetsOperation}. */\nexport interface ListRenameCascadeTargetsParams {\n readonly entityType: RenamableEntityType;\n readonly entityId: string;\n /** Present for User and Role; absent for Tenant. */\n readonly tenantId?: string;\n readonly oldName: string;\n readonly newName: string;\n /** Pre-computed via `extractLabel`; consumers do not re-normalize. */\n readonly oldNormalizedName: string;\n readonly newNormalizedName: string;\n /** Per-stream cursor map from the previous page (start of run is `{}`). */\n readonly cursors?: RenameCascadeCursorMap;\n /** Per-stream per-page item limit. Defaults to 100 (matches chunk size cap). */\n readonly limit?: number;\n /** Optional table-name override; resolved via env when omitted. */\n readonly tableName?: string;\n}\n\n/** Page returned by {@link listRenameCascadeTargetsOperation}. */\nexport interface ListRenameCascadeTargetsResult {\n readonly targets: ReadonlyArray<RenameCascadeRewriteTarget>;\n readonly cursors: RenameCascadeCursorMap;\n /** `true` when every stream returned `null` — outer loop terminates. */\n readonly exhausted: boolean;\n}\n\nconst DEFAULT_PAGE_SIZE = 100 as const;\n\n/**\n * Stream identifiers used in the cursor map. Each `entityType` walks a\n * different fixed set of streams; the cursor map keeps each at its own\n * position so the cascade can drain them in parallel without re-querying\n * exhausted ones.\n */\nconst STREAMS_FOR_ENTITY_TYPE: Record<\n RenamableEntityType,\n ReadonlyArray<string>\n> = {\n Tenant: [\"membershipUserProjection\", \"roleAssignmentUserProjection\"],\n User: [\n \"membershipUserProjection\",\n \"roleAssignmentUserProjection\",\n \"membershipWorkspaceProjection\",\n \"roleAssignmentWorkspaceProjection\",\n ],\n Role: [\"roleAssignmentUserProjection\", \"roleAssignmentWorkspaceProjection\"],\n};\n\n/**\n * Page through the projection rows affected by a Tenant / User / Role\n * rename. The cascade outer loop calls this in a loop, forwarding the\n * returned `cursors` until `exhausted === true`.\n */\nexport async function listRenameCascadeTargetsOperation(\n params: ListRenameCascadeTargetsParams,\n): Promise<ListRenameCascadeTargetsResult> {\n const {\n entityType,\n entityId,\n tenantId,\n oldName,\n newName,\n oldNormalizedName,\n newNormalizedName,\n cursors = {},\n limit = DEFAULT_PAGE_SIZE,\n tableName,\n } = params;\n\n if (!entityId || entityId.length === 0) {\n throw new Error(\"listRenameCascadeTargetsOperation: entityId is required\");\n }\n\n switch (entityType) {\n case RENAMABLE_ENTITY_TYPE.User:\n return pageUserRename({\n userId: entityId,\n oldNormalizedName,\n newNormalizedName,\n newName,\n cursors,\n limit,\n tableName,\n });\n case RENAMABLE_ENTITY_TYPE.Role:\n return pageRoleRename({\n roleId: entityId,\n tenantId,\n newName,\n cursors,\n limit,\n tableName,\n });\n case RENAMABLE_ENTITY_TYPE.Tenant:\n return pageTenantRename({\n tenantId: entityId,\n oldName,\n newName,\n cursors,\n limit,\n tableName,\n });\n default: {\n const exhaustive: never = entityType;\n throw new Error(\n `listRenameCascadeTargetsOperation: unsupported entityType '${String(\n exhaustive,\n )}'`,\n );\n }\n }\n}\n\n/**\n * User rename — page rows from the four affected projection streams.\n * The SK encodes `<normalizedUserName>` in every stream except the user-\n * projection tenant-lane (pattern #3) which sorts by `<normalizedTenantName>`;\n * tenant-lane rows still need a `denormalizedUserName` attr update so the\n * canonical-record symmetry rule (TR-024 rule 3) holds — but no SK rewrite.\n */\nasync function pageUserRename(params: {\n readonly userId: string;\n readonly oldNormalizedName: string;\n readonly newNormalizedName: string;\n readonly newName: string;\n readonly cursors: RenameCascadeCursorMap;\n readonly limit: number;\n readonly tableName?: string;\n}): Promise<ListRenameCascadeTargetsResult> {\n const { userId, newName, cursors, limit, tableName } = params;\n const service = getDynamoControlService(tableName);\n const nextCursors: RenameCascadeCursorMap = {};\n const targets: Array<RenameCascadeRewriteTarget> = [];\n\n // Stream 1 — Membership user-projection (patterns #3 + #4) under the\n // user's partition. Pattern-#3 (tenant-lane) rows only need an attr\n // update; pattern-#4 (workspace-lane) rows have `denormalizedUserName`\n // as an attr (not in the SK), so no SK rewrite is required for the\n // user-projection lane — the workspace-projection (pattern #2) carries\n // the SK rewrite.\n const muStream = cursors.membershipUserProjection;\n if (muStream !== null) {\n const page = await service.entities.membershipUserProjection.query\n .record({ userId })\n .begins({ sk: \"MEMBERSHIP#\" })\n .go({ cursor: muStream ?? null, limit });\n for (const row of page.data ?? []) {\n // Rebuild the SK with the new (denormalized) name where the SK\n // encodes one. For Membership user-projection neither lane\n // encodes <normalizedUserName>; SK rewrites are unnecessary.\n const oldKey = { userId: row.userId, sk: row.sk };\n const newSk = row.sk; // SK unaffected by a User rename in this stream.\n const newKey = { userId: row.userId, sk: newSk };\n targets.push({\n entity: \"membershipUserProjection\",\n oldKey,\n newKey,\n newItem: {\n ...row,\n sk: newSk,\n denormalizedUserName: newName,\n },\n skRewriteRequired: false,\n });\n }\n nextCursors.membershipUserProjection = page.cursor ?? null;\n } else {\n nextCursors.membershipUserProjection = null;\n }\n\n // Stream 2 — RoleAssignment user-projection (pattern #5) under the\n // user's partition. SK sorts on `<normalizedRoleName>` (not user-name),\n // so a User rename only updates the `denormalizedUserName` attr; no SK\n // rewrite required.\n const raUStream = cursors.roleAssignmentUserProjection;\n if (raUStream !== null) {\n const page = await service.entities.roleAssignmentUserProjection.query\n .record({ userId })\n .begins({ sk: \"ROLEASSIGNMENT#\" })\n .go({ cursor: raUStream ?? null, limit });\n for (const row of page.data ?? []) {\n const oldKey = { userId: row.userId, sk: row.sk };\n const newKey = { userId: row.userId, sk: row.sk };\n targets.push({\n entity: \"roleAssignmentUserProjection\",\n oldKey,\n newKey,\n newItem: {\n ...row,\n denormalizedUserName: newName,\n },\n skRewriteRequired: false,\n });\n }\n nextCursors.roleAssignmentUserProjection = page.cursor ?? null;\n } else {\n nextCursors.roleAssignmentUserProjection = null;\n }\n\n // Streams 3 + 4 — Membership / RoleAssignment workspace-projection\n // rows under every workspace the user is a member of. The\n // workspace-projection SK encodes `<normalizedUserName>`, so these\n // streams require an SK rewrite (delete old + put new).\n //\n // Discovery: list the user's workspace-lane Memberships (pattern #4 —\n // `MEMBERSHIP#WORKSPACE#TID#<tenantId>#<normalizedWorkspaceName>#WID#<workspaceId>#…`)\n // and visit each workspace's partition. We paginate workspace\n // discovery via a dedicated cursor stream so the cascade outer loop\n // can resume mid-discovery.\n const discoveryCursor = cursors.workspaceDiscovery;\n if (discoveryCursor !== null) {\n const discovery = await service.entities.membershipUserProjection.query\n .record({ userId })\n .begins({ sk: \"MEMBERSHIP#WORKSPACE#\" })\n .go({ cursor: discoveryCursor ?? null, limit });\n for (const member of discovery.data ?? []) {\n if (!member.workspaceId || !member.tenantId) {\n continue;\n }\n // Per discovered workspace, page Membership + RoleAssignment\n // workspace-projection rows that match the OLD normalized user\n // name. The cascade only needs to rewrite rows currently keyed by\n // the OLD name; rows already at the new name (partial-replay) are\n // skipped naturally by the `begins_with` filter on\n // `MEMBERSHIP#<oldNormalizedUserName>#`.\n await collectWorkspaceUserRenameTargets({\n service,\n tenantId: member.tenantId,\n workspaceId: member.workspaceId,\n userId,\n oldNormalizedName: params.oldNormalizedName,\n newNormalizedName: params.newNormalizedName,\n newName,\n targets,\n });\n }\n nextCursors.workspaceDiscovery = discovery.cursor ?? null;\n } else {\n nextCursors.workspaceDiscovery = null;\n }\n // The workspace-projection streams themselves never need their own\n // cursor — they are fully drained inside each discovered workspace\n // (per-workspace row counts are small). Mark them exhausted up-front\n // so the outer loop's `exhausted` check ignores them.\n nextCursors.membershipWorkspaceProjection = null;\n nextCursors.roleAssignmentWorkspaceProjection = null;\n\n const exhausted =\n STREAMS_FOR_ENTITY_TYPE.User.every((s) => nextCursors[s] === null) &&\n nextCursors.workspaceDiscovery === null;\n\n return { targets, cursors: nextCursors, exhausted };\n}\n\nasync function collectWorkspaceUserRenameTargets(params: {\n readonly service: ReturnType<typeof getDynamoControlService>;\n readonly tenantId: string;\n readonly workspaceId: string;\n readonly userId: string;\n readonly oldNormalizedName: string;\n readonly newNormalizedName: string;\n readonly newName: string;\n readonly targets: Array<RenameCascadeRewriteTarget>;\n}): Promise<void> {\n const {\n service,\n tenantId,\n workspaceId,\n userId,\n oldNormalizedName,\n newName,\n targets,\n } = params;\n\n // Membership workspace-projection (pattern #2) — SK is\n // `MEMBERSHIP#<normalizedUserName>#USER#<userId>#<membershipId>`.\n const mwPage = await service.entities.membershipWorkspaceProjection.query\n .record({ tenantId, workspaceId })\n .begins({ sk: `MEMBERSHIP#${oldNormalizedName}#USER#${userId}#` })\n .go({});\n for (const row of mwPage.data ?? []) {\n const newSk = buildMembershipWorkspaceProjectionSk({\n userId: row.userId,\n membershipId: row.membershipId,\n denormalizedUserName: newName,\n });\n targets.push({\n entity: \"membershipWorkspaceProjection\",\n oldKey: {\n tenantId: row.tenantId,\n workspaceId: row.workspaceId,\n sk: row.sk,\n },\n newKey: {\n tenantId: row.tenantId,\n workspaceId: row.workspaceId,\n sk: newSk,\n },\n newItem: {\n ...row,\n sk: newSk,\n denormalizedUserName: newName,\n },\n skRewriteRequired: row.sk !== newSk,\n });\n }\n\n // RoleAssignment workspace-projection (pattern #9) — SK is\n // `ROLEASSIGNMENT#<roleId>#<normalizedUserName>#USER#<userId>#…`.\n // `<roleId>` discriminates first, so we can't prefix-scan on user-name\n // alone — list all of this user's workspace-projection rows by paging\n // through `ROLEASSIGNMENT#` and filtering on the userId server-side via\n // ElectroDB's `.where()` builder.\n const raPage = await service.entities.roleAssignmentWorkspaceProjection.query\n .record({ tenantId, workspaceId })\n .begins({ sk: \"ROLEASSIGNMENT#\" })\n .where((attr, op) => op.eq(attr.userId, userId))\n .go({});\n for (const row of raPage.data ?? []) {\n const newSk = buildRoleAssignmentWorkspaceProjectionSk({\n roleId: row.roleId,\n userId: row.userId,\n roleAssignmentId: row.roleAssignmentId,\n denormalizedUserName: newName,\n });\n targets.push({\n entity: \"roleAssignmentWorkspaceProjection\",\n oldKey: {\n tenantId: row.tenantId,\n workspaceId: row.workspaceId,\n sk: row.sk,\n },\n newKey: {\n tenantId: row.tenantId,\n workspaceId: row.workspaceId,\n sk: newSk,\n },\n newItem: {\n ...row,\n sk: newSk,\n denormalizedUserName: newName,\n },\n skRewriteRequired: row.sk !== newSk,\n });\n }\n}\n\n/**\n * Role rename — SK rewrites are required on the RoleAssignment user-\n * projection (pattern #5 encodes `<normalizedRoleName>` in the SK).\n * RoleAssignment workspace-projection (pattern #9) sorts on raw\n * `<roleId>` — only an attr update.\n *\n * Affected users are discovered via the canonical RoleAssignment GSI1\n * (`<roleId>#` prefix). For #1023 the discovery walks the GSI1 page;\n * the cascade outer loop pages through it via the `roleDiscovery`\n * cursor.\n */\nasync function pageRoleRename(params: {\n readonly roleId: string;\n readonly tenantId?: string;\n readonly newName: string;\n readonly cursors: RenameCascadeCursorMap;\n readonly limit: number;\n readonly tableName?: string;\n}): Promise<ListRenameCascadeTargetsResult> {\n const { roleId, tenantId, newName, cursors, limit, tableName } = params;\n if (!tenantId) {\n throw new Error(\n \"listRenameCascadeTargetsOperation: tenantId is required for Role rename\",\n );\n }\n\n const service = getDynamoControlService(tableName);\n const nextCursors: RenameCascadeCursorMap = {};\n const targets: Array<RenameCascadeRewriteTarget> = [];\n\n // Discovery — page canonical RoleAssignment rows for this role via\n // GSI1 (`<roleId>#` prefix on the discriminator-first GSI1SK). GSI1\n // is sharded; for #1023 v1 we walk shard 0 only — same follow-up\n // note as the Tenant rename discovery path.\n const discoveryCursor = cursors.roleDiscovery;\n if (discoveryCursor !== null) {\n const page = await service.entities.roleAssignment.query\n .gsi1({ tenantId, gsi1Shard: \"0\" })\n .begins({ gsi1sk: `${roleId}#` })\n .go({ cursor: discoveryCursor ?? null, limit });\n\n for (const row of page.data ?? []) {\n const userId = extractUserIdFromResource(row.resource);\n if (userId === undefined) {\n // Cannot resolve the row to a user partition — skip. The cascade\n // outer loop logs and continues; a follow-up sweep can re-process.\n continue;\n }\n // Per affected user, rewrite the user-projection rows for this\n // role (pattern #5). The SK encodes `<normalizedRoleName>` so we\n // need to read the user-projection row(s) for this role and\n // rewrite their SKs.\n await collectUserRoleRenameTargets({\n service,\n userId,\n roleId,\n newName,\n targets,\n });\n }\n nextCursors.roleDiscovery = page.cursor ?? null;\n } else {\n nextCursors.roleDiscovery = null;\n }\n nextCursors.roleAssignmentUserProjection = null;\n nextCursors.roleAssignmentWorkspaceProjection = null;\n\n const exhausted = nextCursors.roleDiscovery === null;\n\n return { targets, cursors: nextCursors, exhausted };\n}\n\nasync function collectUserRoleRenameTargets(params: {\n readonly service: ReturnType<typeof getDynamoControlService>;\n readonly userId: string;\n readonly roleId: string;\n readonly newName: string;\n readonly targets: Array<RenameCascadeRewriteTarget>;\n}): Promise<void> {\n const { service, userId, roleId, newName, targets } = params;\n\n // User-projection (pattern #5) — SK encodes `<normalizedRoleName>`,\n // discriminator on TENANT / WORKSPACE prefix. Walk both lanes for the\n // affected role: server-side filter on `roleId` (the discriminator\n // sits after the normalized role name so a single prefix can't narrow\n // by roleId without the normalized name).\n const userProjPage = await service.entities.roleAssignmentUserProjection.query\n .record({ userId })\n .begins({ sk: \"ROLEASSIGNMENT#\" })\n .where((attr, op) => op.eq(attr.roleId, roleId))\n .go({});\n\n for (const row of userProjPage.data ?? []) {\n const isWorkspaceLane =\n typeof row.workspaceId === \"string\" && row.workspaceId.length > 0;\n const newSk = isWorkspaceLane\n ? buildRoleAssignmentUserProjectionSkWorkspaceLane({\n tenantId: row.tenantId,\n workspaceId: row.workspaceId as string,\n roleId: row.roleId,\n roleAssignmentId: row.roleAssignmentId,\n denormalizedRoleName: newName,\n })\n : buildRoleAssignmentUserProjectionSkTenantLane({\n tenantId: row.tenantId,\n roleId: row.roleId,\n roleAssignmentId: row.roleAssignmentId,\n denormalizedRoleName: newName,\n });\n targets.push({\n entity: \"roleAssignmentUserProjection\",\n oldKey: { userId: row.userId, sk: row.sk },\n newKey: { userId: row.userId, sk: newSk },\n newItem: {\n ...row,\n sk: newSk,\n denormalizedRoleName: newName,\n },\n skRewriteRequired: row.sk !== newSk,\n });\n }\n}\n\n/**\n * Tenant rename — only the `denormalizedTenantName` attr updates on\n * affected rows. SKs never carry tenant-name in the OpenHI grammar (the\n * Membership user-projection tenant-lane SK encodes `<normalizedTenantName>`\n * — see pattern #3 — so that single sub-lane DOES need an SK rewrite).\n *\n * Discovery: page canonical Memberships for this tenant via GSI1, then\n * for each affected user enumerate their pattern-#3 user-projection rows\n * (those that key off `<normalizedTenantName>`).\n */\nasync function pageTenantRename(params: {\n readonly tenantId: string;\n readonly oldName: string;\n readonly newName: string;\n readonly cursors: RenameCascadeCursorMap;\n readonly limit: number;\n readonly tableName?: string;\n}): Promise<ListRenameCascadeTargetsResult> {\n const { tenantId, newName, cursors, limit, tableName } = params;\n const service = getDynamoControlService(tableName);\n const nextCursors: RenameCascadeCursorMap = {};\n const targets: Array<RenameCascadeRewriteTarget> = [];\n\n // Discovery — page canonical Memberships for this tenant via GSI1.\n // GSI1 is sharded; for #1023 v1 we walk shard 0 only. Multi-shard\n // discovery is a tight follow-up — it requires either iterating\n // shards in a fan-out (4 queries per page) or threading a per-shard\n // cursor map. The current implementation accepts coverage limited\n // to shard 0; large tenants will need the follow-up.\n const discoveryCursor = cursors.tenantDiscovery;\n if (discoveryCursor !== null) {\n const page = await service.entities.membership.query\n .gsi1({ tenantId, gsi1Shard: \"0\" })\n .go({ cursor: discoveryCursor ?? null, limit });\n\n for (const row of page.data ?? []) {\n const userId = extractUserIdFromResource(row.resource);\n if (userId === undefined) {\n continue;\n }\n // Per affected user, rewrite the pattern-#3 user-projection row\n // for this tenant. The SK is\n // `MEMBERSHIP#TENANT#<normalizedTenantName>#TID#<tenantId>#<id>`.\n const userPage = await service.entities.membershipUserProjection.query\n .record({ userId })\n .begins({ sk: `MEMBERSHIP#TENANT#` })\n .where((attr, op) => op.eq(attr.tenantId, tenantId))\n .go({});\n for (const userRow of userPage.data ?? []) {\n const newSk = buildMembershipUserProjectionSkTenantLane({\n tenantId: userRow.tenantId,\n membershipId: userRow.membershipId,\n denormalizedTenantName: newName,\n });\n targets.push({\n entity: \"membershipUserProjection\",\n oldKey: { userId: userRow.userId, sk: userRow.sk },\n newKey: { userId: userRow.userId, sk: newSk },\n newItem: {\n ...userRow,\n sk: newSk,\n denormalizedTenantName: newName,\n },\n skRewriteRequired: userRow.sk !== newSk,\n });\n }\n // Pattern #4 workspace-lane user-projection rows carry\n // `denormalizedTenantName` as an attr only — no SK rewrite.\n const wsPage = await service.entities.membershipUserProjection.query\n .record({ userId })\n .begins({ sk: `MEMBERSHIP#WORKSPACE#TID#${tenantId}#` })\n .go({});\n for (const wsRow of wsPage.data ?? []) {\n // SK stays the same — pattern #4 encodes tenant by raw\n // `<tenantId>`, not name.\n const newSk = buildMembershipUserProjectionSkWorkspaceLane({\n tenantId: wsRow.tenantId,\n workspaceId: wsRow.workspaceId as string,\n membershipId: wsRow.membershipId,\n denormalizedWorkspaceName: wsRow.denormalizedWorkspaceName,\n });\n targets.push({\n entity: \"membershipUserProjection\",\n oldKey: { userId: wsRow.userId, sk: wsRow.sk },\n newKey: { userId: wsRow.userId, sk: newSk },\n newItem: {\n ...wsRow,\n sk: newSk,\n denormalizedTenantName: newName,\n },\n skRewriteRequired: wsRow.sk !== newSk,\n });\n }\n }\n nextCursors.tenantDiscovery = page.cursor ?? null;\n } else {\n nextCursors.tenantDiscovery = null;\n }\n nextCursors.membershipUserProjection = null;\n nextCursors.roleAssignmentUserProjection = null;\n\n const exhausted = nextCursors.tenantDiscovery === null;\n\n return { targets, cursors: nextCursors, exhausted };\n}\n\n/**\n * Extract `userId` from a canonical Membership / RoleAssignment resource\n * JSON string. The canonical row stores the user reference inside the\n * resource (FHIR `Reference` shape — `\\{ \"user\": \\{ \"reference\": \"User/<id>\" \\} \\}`);\n * discovery via GSI1 returns the canonical row, and the cascade needs\n * `userId` to address the user partition for projection rewrites.\n * Returns `undefined` when the field is missing or malformed so the\n * cascade can skip rows it cannot resolve.\n */\nfunction extractUserIdFromResource(resource: unknown): string | undefined {\n if (typeof resource !== \"string\" || resource.length === 0) {\n return undefined;\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(resource);\n } catch {\n return undefined;\n }\n if (!parsed || typeof parsed !== \"object\") {\n return undefined;\n }\n return extractReferenceSlug(parsed as Record<string, unknown>, \"user\");\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/workflows/control-plane/rename-cascade/rename-list-targets.handler.ts","../src/data/operations/control/rename-cascade/rename-cascade-list-targets-operation.ts"],"sourcesContent":["/**\n * Cascade Step Functions handler that queries one page of projection\n * rows affected by a Tenant / User / Role rename and bundles them into\n * \\<=50-target chunks for the downstream Distributed Map state.\n *\n * One invocation per outer-loop iteration:\n *\n * 1. Calls `listRenameCascadeTargetsOperation` with the per-stream\n * cursor map from the prior iteration (`{}` on the first call).\n * 2. Splits the merged page into chunks via `chunkRenameCascadeTargets`.\n * 3. Stamps each chunk with a deterministic `chunkToken` so a replayed\n * Map iteration lands idempotently via `executeMultiWrite`'s\n * `ClientRequestToken` forwarding.\n * 4. Returns the chunks, the new cursors, and the cumulative metrics\n * so the state machine's outer `Choice` knows when to stop.\n *\n * The handler itself NEVER touches the canonical Tenant / User / Role\n * record; the cascade is a consumer that only rewrites projection rows.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport type {\n RenameCascadeChunkInput,\n RenameCascadeListInput,\n RenameCascadeListOutput,\n} from \"./events\";\nimport {\n type RenameCascadeCursorMap,\n listRenameCascadeTargetsOperation,\n} from \"../../../data/operations/control/rename-cascade/rename-cascade-list-targets-operation\";\nimport { chunkRenameCascadeTargets } from \"../../../data/operations/control/rename-cascade/rename-cascade-rewrite-chunk-operation\";\n\nexport const handler = async (\n input: RenameCascadeListInput,\n): Promise<RenameCascadeListOutput> => {\n const cursors: RenameCascadeCursorMap = {};\n if (input.cursors) {\n for (const [key, value] of Object.entries(input.cursors)) {\n cursors[key] = value;\n }\n }\n\n const page = await listRenameCascadeTargetsOperation({\n entityType: input.entityType,\n entityId: input.entityId,\n tenantId: input.tenantId,\n oldName: input.oldName,\n newName: input.newName,\n oldNormalizedName: input.oldNormalizedName,\n newNormalizedName: input.newNormalizedName,\n cursors,\n });\n\n const chunks: Array<RenameCascadeChunkInput> = chunkRenameCascadeTargets(\n page.targets,\n ).map((targets) => ({\n entityType: input.entityType,\n entityId: input.entityId,\n tenantId: input.tenantId,\n targets,\n chunkToken: randomUUID(),\n }));\n\n const priorRewritten = input.itemsRewritten ?? 0;\n const priorChunks = input.chunkCount ?? 0;\n const itemsRewritten = priorRewritten + page.targets.length;\n const chunkCount = priorChunks + chunks.length;\n\n return {\n entityType: input.entityType,\n entityId: input.entityId,\n tenantId: input.tenantId,\n oldName: input.oldName,\n newName: input.newName,\n oldNormalizedName: input.oldNormalizedName,\n newNormalizedName: input.newNormalizedName,\n cursors: page.cursors,\n chunks,\n exhausted: page.exhausted,\n itemsRewritten,\n chunkCount,\n };\n};\n","/**\n * Enumerate projection rows affected by a Tenant / User / Role rename\n * for the TR-023 rename cascade.\n *\n * One page per call; the cascade state machine outer loop walks the\n * returned `cursors` map back into this operation until every per-entity\n * stream returns `null`. Each emitted row carries:\n *\n * - the projection-entity name (so the rewrite-chunk operation can map\n * it to the correct ElectroDB entity in `executeMultiWrite`),\n * - the **existing** composite key (used for the `delete` triple in the\n * transact-write pair),\n * - the **new** composite key (used for the `put` triple — same row\n * identity but a rewritten SK when the SK encodes the renamed\n * normalized name), and\n * - the row's existing attributes (carried verbatim into the `put` so\n * `summary`, `vid`, `lastUpdated`, etc. are preserved across the\n * rewrite), with the renamed `denormalized<CarrierEntity>Name`\n * replaced by the new display name.\n *\n * Per-entityType query plan (per the ADR-018 implementation guide § 5):\n *\n * - **User rename**: under `PK = USER#ID#<userId>` — Membership user-\n * projection rows (patterns #3 + #4) and RoleAssignment user-projection\n * rows (pattern #5). Workspace-side projection rows\n * (membershipWorkspaceProjection #2 + roleAssignmentWorkspaceProjection\n * #9) encode `<normalizedUserName>` in their SK; this operation\n * discovers the affected workspaces from the user's pattern-#4\n * memberships and queries each workspace partition for them.\n * - **Role rename**: under every affected user partition — RoleAssignment\n * user-projection rows (pattern #5) sort on `<normalizedRoleName>` and\n * need a SK rewrite. RoleAssignment canonical (pattern #8) and\n * workspace-projection (pattern #9) sort on raw `<roleId>` so only the\n * denormalized attr changes (no SK rewrite). The affected user-ids\n * are discovered via the canonical RoleAssignment GSI1 (`<roleId>#`\n * prefix).\n * - **Tenant rename**: only `denormalizedTenantName` updates — SKs do\n * not carry tenant-name; the row identity is preserved. Affected user-\n * ids are discovered via the canonical Membership GSI1 page.\n *\n * For #1023 the User-rename path is implemented in full; the Tenant /\n * Role discovery hooks are scaffolded with the right query shape and\n * cursor map but only walk one canonical discovery batch per call (the\n * cascade outer loop pages through them). See § 5 of the implementation\n * guide for the full matrix.\n *\n * @see .state/adr-018-implementation-guide.md § 5 (TR-023 Rename-Cascade Consumer Contract)\n * @see .claude/rules/data-layer-layout.md\n */\n\nimport {\n RENAMABLE_ENTITY_TYPE,\n type RenamableEntityType,\n} from \"@openhi/workflows\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport {\n buildMembershipUserProjectionSkTenantLane,\n buildMembershipUserProjectionSkWorkspaceLane,\n extractReferenceSlug,\n} from \"../membership/membership-user-projection\";\nimport { buildMembershipWorkspaceProjectionSk } from \"../membership/membership-workspace-projection\";\nimport {\n buildRoleAssignmentUserProjectionSkTenantLane,\n buildRoleAssignmentUserProjectionSkWorkspaceLane,\n} from \"../roleassignment/roleassignment-user-projection\";\nimport { buildRoleAssignmentWorkspaceProjectionSk } from \"../roleassignment/roleassignment-workspace-projection\";\n\n/**\n * Projection-entity name keys this operation may emit. Each key maps to\n * an entity in the control-plane service; the rewrite-chunk consumer\n * forwards it to `executeMultiWrite` as the `entity` field on a triple.\n */\nexport const RENAME_CASCADE_PROJECTION_ENTITY = {\n MembershipUserProjection: \"membershipUserProjection\",\n MembershipWorkspaceProjection: \"membershipWorkspaceProjection\",\n RoleAssignmentUserProjection: \"roleAssignmentUserProjection\",\n RoleAssignmentWorkspaceProjection: \"roleAssignmentWorkspaceProjection\",\n} as const;\nexport type RenameCascadeProjectionEntity =\n (typeof RENAME_CASCADE_PROJECTION_ENTITY)[keyof typeof RENAME_CASCADE_PROJECTION_ENTITY];\n\n/**\n * One row to rewrite — the cascade rewrite-chunk operation turns each\n * entry into a `delete oldKey` + `put newPayload` transact-write pair.\n *\n * `oldKey` and `newKey` differ only in the SK segment when the SK\n * encodes a normalized form of the renamed name. For Tenant rename and\n * for SK-stable RoleAssignment projections (canonical pattern #8 and\n * workspace pattern #9 under a Role rename), `oldKey === newKey` and\n * the rewrite collapses to a single `put` overwrite.\n */\nexport interface RenameCascadeRewriteTarget {\n readonly entity: RenameCascadeProjectionEntity;\n /** Composite key payload for the existing row. */\n readonly oldKey: Record<string, string>;\n /** Composite key payload for the rewritten row. */\n readonly newKey: Record<string, string>;\n /**\n * Full row payload to write at `newKey` — carries the existing\n * `summary`, `vid`, `lastUpdated`, and discriminating fields, with\n * the renamed `denormalized<CarrierEntity>Name` swapped to the new\n * display name.\n */\n readonly newItem: Record<string, unknown>;\n /**\n * `true` when `oldKey` and `newKey` differ — the rewrite must atomic\n * delete the old row and put the new row in the same transaction.\n * `false` when only the denormalized attr changes — a single `put`\n * overwrite is sufficient.\n */\n readonly skRewriteRequired: boolean;\n}\n\n/** Per-stream cursor — `null` marks a stream as exhausted. */\nexport type RenameCascadeCursorMap = Record<string, string | null>;\n\n/** Inputs accepted by {@link listRenameCascadeTargetsOperation}. */\nexport interface ListRenameCascadeTargetsParams {\n readonly entityType: RenamableEntityType;\n readonly entityId: string;\n /** Present for User and Role; absent for Tenant. */\n readonly tenantId?: string;\n readonly oldName: string;\n readonly newName: string;\n /** Pre-computed via `extractLabel`; consumers do not re-normalize. */\n readonly oldNormalizedName: string;\n readonly newNormalizedName: string;\n /** Per-stream cursor map from the previous page (start of run is `{}`). */\n readonly cursors?: RenameCascadeCursorMap;\n /** Per-stream per-page item limit. Defaults to 100 (matches chunk size cap). */\n readonly limit?: number;\n /** Optional table-name override; resolved via env when omitted. */\n readonly tableName?: string;\n}\n\n/** Page returned by {@link listRenameCascadeTargetsOperation}. */\nexport interface ListRenameCascadeTargetsResult {\n readonly targets: ReadonlyArray<RenameCascadeRewriteTarget>;\n readonly cursors: RenameCascadeCursorMap;\n /** `true` when every stream returned `null` — outer loop terminates. */\n readonly exhausted: boolean;\n}\n\nconst DEFAULT_PAGE_SIZE = 100 as const;\n\n/**\n * Stream identifiers used in the cursor map. Each `entityType` walks a\n * different fixed set of streams; the cursor map keeps each at its own\n * position so the cascade can drain them in parallel without re-querying\n * exhausted ones.\n */\nconst STREAMS_FOR_ENTITY_TYPE: Record<\n RenamableEntityType,\n ReadonlyArray<string>\n> = {\n Tenant: [\"membershipUserProjection\", \"roleAssignmentUserProjection\"],\n User: [\n \"membershipUserProjection\",\n \"roleAssignmentUserProjection\",\n \"membershipWorkspaceProjection\",\n \"roleAssignmentWorkspaceProjection\",\n ],\n Role: [\"roleAssignmentUserProjection\", \"roleAssignmentWorkspaceProjection\"],\n};\n\n/**\n * Page through the projection rows affected by a Tenant / User / Role\n * rename. The cascade outer loop calls this in a loop, forwarding the\n * returned `cursors` until `exhausted === true`.\n */\nexport async function listRenameCascadeTargetsOperation(\n params: ListRenameCascadeTargetsParams,\n): Promise<ListRenameCascadeTargetsResult> {\n const {\n entityType,\n entityId,\n tenantId,\n oldName,\n newName,\n oldNormalizedName,\n newNormalizedName,\n cursors = {},\n limit = DEFAULT_PAGE_SIZE,\n tableName,\n } = params;\n\n if (!entityId || entityId.length === 0) {\n throw new Error(\"listRenameCascadeTargetsOperation: entityId is required\");\n }\n\n switch (entityType) {\n case RENAMABLE_ENTITY_TYPE.User:\n return pageUserRename({\n userId: entityId,\n oldNormalizedName,\n newNormalizedName,\n newName,\n cursors,\n limit,\n tableName,\n });\n case RENAMABLE_ENTITY_TYPE.Role:\n return pageRoleRename({\n roleId: entityId,\n tenantId,\n newName,\n cursors,\n limit,\n tableName,\n });\n case RENAMABLE_ENTITY_TYPE.Tenant:\n return pageTenantRename({\n tenantId: entityId,\n oldName,\n newName,\n cursors,\n limit,\n tableName,\n });\n default: {\n const exhaustive: never = entityType;\n throw new Error(\n `listRenameCascadeTargetsOperation: unsupported entityType '${String(\n exhaustive,\n )}'`,\n );\n }\n }\n}\n\n/**\n * User rename — page rows from the four affected projection streams.\n * The SK encodes `<normalizedUserName>` in every stream except the user-\n * projection tenant-lane (pattern #3) which sorts by `<normalizedTenantName>`;\n * tenant-lane rows still need a `denormalizedUserName` attr update so the\n * canonical-record symmetry rule (TR-024 rule 3) holds — but no SK rewrite.\n */\nasync function pageUserRename(params: {\n readonly userId: string;\n readonly oldNormalizedName: string;\n readonly newNormalizedName: string;\n readonly newName: string;\n readonly cursors: RenameCascadeCursorMap;\n readonly limit: number;\n readonly tableName?: string;\n}): Promise<ListRenameCascadeTargetsResult> {\n const { userId, newName, cursors, limit, tableName } = params;\n const service = getDynamoControlService(tableName);\n const nextCursors: RenameCascadeCursorMap = {};\n const targets: Array<RenameCascadeRewriteTarget> = [];\n\n // Stream 1 — Membership user-projection (patterns #3 + #4) under the\n // user's partition. Pattern-#3 (tenant-lane) rows only need an attr\n // update; pattern-#4 (workspace-lane) rows have `denormalizedUserName`\n // as an attr (not in the SK), so no SK rewrite is required for the\n // user-projection lane — the workspace-projection (pattern #2) carries\n // the SK rewrite.\n const muStream = cursors.membershipUserProjection;\n if (muStream !== null) {\n const page = await service.entities.membershipUserProjection.query\n .record({ userId })\n .begins({ sk: \"MEMBERSHIP#\" })\n .go({ cursor: muStream ?? null, limit });\n for (const row of page.data ?? []) {\n // Rebuild the SK with the new (denormalized) name where the SK\n // encodes one. For Membership user-projection neither lane\n // encodes <normalizedUserName>; SK rewrites are unnecessary.\n const oldKey = { userId: row.userId, sk: row.sk };\n const newSk = row.sk; // SK unaffected by a User rename in this stream.\n const newKey = { userId: row.userId, sk: newSk };\n targets.push({\n entity: \"membershipUserProjection\",\n oldKey,\n newKey,\n newItem: {\n ...row,\n sk: newSk,\n denormalizedUserName: newName,\n },\n skRewriteRequired: false,\n });\n }\n nextCursors.membershipUserProjection = page.cursor ?? null;\n } else {\n nextCursors.membershipUserProjection = null;\n }\n\n // Stream 2 — RoleAssignment user-projection (pattern #5) under the\n // user's partition. SK sorts on `<normalizedRoleName>` (not user-name),\n // so a User rename only updates the `denormalizedUserName` attr; no SK\n // rewrite required.\n const raUStream = cursors.roleAssignmentUserProjection;\n if (raUStream !== null) {\n const page = await service.entities.roleAssignmentUserProjection.query\n .record({ userId })\n .begins({ sk: \"ROLEASSIGNMENT#\" })\n .go({ cursor: raUStream ?? null, limit });\n for (const row of page.data ?? []) {\n const oldKey = { userId: row.userId, sk: row.sk };\n const newKey = { userId: row.userId, sk: row.sk };\n targets.push({\n entity: \"roleAssignmentUserProjection\",\n oldKey,\n newKey,\n newItem: {\n ...row,\n denormalizedUserName: newName,\n },\n skRewriteRequired: false,\n });\n }\n nextCursors.roleAssignmentUserProjection = page.cursor ?? null;\n } else {\n nextCursors.roleAssignmentUserProjection = null;\n }\n\n // Streams 3 + 4 — Membership / RoleAssignment workspace-projection\n // rows under every workspace the user is a member of. The\n // workspace-projection SK encodes `<normalizedUserName>`, so these\n // streams require an SK rewrite (delete old + put new).\n //\n // Discovery: list the user's workspace-lane Memberships (pattern #4 —\n // `MEMBERSHIP#WORKSPACE#TID#<tenantId>#<normalizedWorkspaceName>#WID#<workspaceId>#…`)\n // and visit each workspace's partition. We paginate workspace\n // discovery via a dedicated cursor stream so the cascade outer loop\n // can resume mid-discovery.\n const discoveryCursor = cursors.workspaceDiscovery;\n if (discoveryCursor !== null) {\n const discovery = await service.entities.membershipUserProjection.query\n .record({ userId })\n .begins({ sk: \"MEMBERSHIP#WORKSPACE#\" })\n .go({ cursor: discoveryCursor ?? null, limit });\n for (const member of discovery.data ?? []) {\n if (!member.workspaceId || !member.tenantId) {\n continue;\n }\n // Per discovered workspace, page Membership + RoleAssignment\n // workspace-projection rows that match the OLD normalized user\n // name. The cascade only needs to rewrite rows currently keyed by\n // the OLD name; rows already at the new name (partial-replay) are\n // skipped naturally by the `begins_with` filter on\n // `MEMBERSHIP#<oldNormalizedUserName>#`.\n await collectWorkspaceUserRenameTargets({\n service,\n tenantId: member.tenantId,\n workspaceId: member.workspaceId,\n userId,\n oldNormalizedName: params.oldNormalizedName,\n newNormalizedName: params.newNormalizedName,\n newName,\n targets,\n });\n }\n nextCursors.workspaceDiscovery = discovery.cursor ?? null;\n } else {\n nextCursors.workspaceDiscovery = null;\n }\n // The workspace-projection streams themselves never need their own\n // cursor — they are fully drained inside each discovered workspace\n // (per-workspace row counts are small). Mark them exhausted up-front\n // so the outer loop's `exhausted` check ignores them.\n nextCursors.membershipWorkspaceProjection = null;\n nextCursors.roleAssignmentWorkspaceProjection = null;\n\n const exhausted =\n STREAMS_FOR_ENTITY_TYPE.User.every((s) => nextCursors[s] === null) &&\n nextCursors.workspaceDiscovery === null;\n\n return { targets, cursors: nextCursors, exhausted };\n}\n\nasync function collectWorkspaceUserRenameTargets(params: {\n readonly service: ReturnType<typeof getDynamoControlService>;\n readonly tenantId: string;\n readonly workspaceId: string;\n readonly userId: string;\n readonly oldNormalizedName: string;\n readonly newNormalizedName: string;\n readonly newName: string;\n readonly targets: Array<RenameCascadeRewriteTarget>;\n}): Promise<void> {\n const {\n service,\n tenantId,\n workspaceId,\n userId,\n oldNormalizedName,\n newName,\n targets,\n } = params;\n\n // Membership workspace-projection (pattern #2) — SK is\n // `MEMBERSHIP#<normalizedUserName>#USER#<userId>#<membershipId>`.\n const mwPage = await service.entities.membershipWorkspaceProjection.query\n .record({ tenantId, workspaceId })\n .begins({ sk: `MEMBERSHIP#${oldNormalizedName}#USER#${userId}#` })\n .go({});\n for (const row of mwPage.data ?? []) {\n const newSk = buildMembershipWorkspaceProjectionSk({\n userId: row.userId,\n membershipId: row.membershipId,\n denormalizedUserName: newName,\n });\n targets.push({\n entity: \"membershipWorkspaceProjection\",\n oldKey: {\n tenantId: row.tenantId,\n workspaceId: row.workspaceId,\n sk: row.sk,\n },\n newKey: {\n tenantId: row.tenantId,\n workspaceId: row.workspaceId,\n sk: newSk,\n },\n newItem: {\n ...row,\n sk: newSk,\n denormalizedUserName: newName,\n },\n skRewriteRequired: row.sk !== newSk,\n });\n }\n\n // RoleAssignment workspace-projection (pattern #9) — SK is\n // `ROLEASSIGNMENT#<roleId>#<normalizedUserName>#USER#<userId>#…`.\n // `<roleId>` discriminates first, so we can't prefix-scan on user-name\n // alone — list all of this user's workspace-projection rows by paging\n // through `ROLEASSIGNMENT#` and filtering on the userId server-side via\n // ElectroDB's `.where()` builder.\n const raPage = await service.entities.roleAssignmentWorkspaceProjection.query\n .record({ tenantId, workspaceId })\n .begins({ sk: \"ROLEASSIGNMENT#\" })\n .where((attr, op) => op.eq(attr.userId, userId))\n .go({});\n for (const row of raPage.data ?? []) {\n const newSk = buildRoleAssignmentWorkspaceProjectionSk({\n roleId: row.roleId,\n userId: row.userId,\n roleAssignmentId: row.roleAssignmentId,\n denormalizedUserName: newName,\n });\n targets.push({\n entity: \"roleAssignmentWorkspaceProjection\",\n oldKey: {\n tenantId: row.tenantId,\n workspaceId: row.workspaceId,\n sk: row.sk,\n },\n newKey: {\n tenantId: row.tenantId,\n workspaceId: row.workspaceId,\n sk: newSk,\n },\n newItem: {\n ...row,\n sk: newSk,\n denormalizedUserName: newName,\n },\n skRewriteRequired: row.sk !== newSk,\n });\n }\n}\n\n/**\n * Role rename — SK rewrites are required on the RoleAssignment user-\n * projection (pattern #5 encodes `<normalizedRoleName>` in the SK).\n * RoleAssignment workspace-projection (pattern #9) sorts on raw\n * `<roleId>` — only an attr update.\n *\n * Affected users are discovered via the canonical RoleAssignment GSI1\n * (`<roleId>#` prefix). For #1023 the discovery walks the GSI1 page;\n * the cascade outer loop pages through it via the `roleDiscovery`\n * cursor.\n */\nasync function pageRoleRename(params: {\n readonly roleId: string;\n readonly tenantId?: string;\n readonly newName: string;\n readonly cursors: RenameCascadeCursorMap;\n readonly limit: number;\n readonly tableName?: string;\n}): Promise<ListRenameCascadeTargetsResult> {\n const { roleId, tenantId, newName, cursors, limit, tableName } = params;\n if (!tenantId) {\n throw new Error(\n \"listRenameCascadeTargetsOperation: tenantId is required for Role rename\",\n );\n }\n\n const service = getDynamoControlService(tableName);\n const nextCursors: RenameCascadeCursorMap = {};\n const targets: Array<RenameCascadeRewriteTarget> = [];\n\n // Discovery — page canonical RoleAssignment rows for this role via\n // GSI1 (`<roleId>#` prefix on the discriminator-first GSI1SK). GSI1\n // is sharded; for #1023 v1 we walk shard 0 only — same follow-up\n // note as the Tenant rename discovery path.\n const discoveryCursor = cursors.roleDiscovery;\n if (discoveryCursor !== null) {\n const page = await service.entities.roleAssignment.query\n .gsi1({ tenantId, gsi1Shard: \"0\" })\n .begins({ gsi1sk: `${roleId}#` })\n .go({ cursor: discoveryCursor ?? null, limit });\n\n for (const row of page.data ?? []) {\n const userId = extractUserIdFromResource(row.resource);\n if (userId === undefined) {\n // Cannot resolve the row to a user partition — skip. The cascade\n // outer loop logs and continues; a follow-up sweep can re-process.\n continue;\n }\n // Per affected user, rewrite the user-projection rows for this\n // role (pattern #5). The SK encodes `<normalizedRoleName>` so we\n // need to read the user-projection row(s) for this role and\n // rewrite their SKs.\n await collectUserRoleRenameTargets({\n service,\n userId,\n roleId,\n newName,\n targets,\n });\n }\n nextCursors.roleDiscovery = page.cursor ?? null;\n } else {\n nextCursors.roleDiscovery = null;\n }\n nextCursors.roleAssignmentUserProjection = null;\n nextCursors.roleAssignmentWorkspaceProjection = null;\n\n const exhausted = nextCursors.roleDiscovery === null;\n\n return { targets, cursors: nextCursors, exhausted };\n}\n\nasync function collectUserRoleRenameTargets(params: {\n readonly service: ReturnType<typeof getDynamoControlService>;\n readonly userId: string;\n readonly roleId: string;\n readonly newName: string;\n readonly targets: Array<RenameCascadeRewriteTarget>;\n}): Promise<void> {\n const { service, userId, roleId, newName, targets } = params;\n\n // User-projection (pattern #5) — SK encodes `<normalizedRoleName>`,\n // discriminator on TENANT / WORKSPACE prefix. Walk both lanes for the\n // affected role: server-side filter on `roleId` (the discriminator\n // sits after the normalized role name so a single prefix can't narrow\n // by roleId without the normalized name).\n const userProjPage = await service.entities.roleAssignmentUserProjection.query\n .record({ userId })\n .begins({ sk: \"ROLEASSIGNMENT#\" })\n .where((attr, op) => op.eq(attr.roleId, roleId))\n .go({});\n\n for (const row of userProjPage.data ?? []) {\n const isWorkspaceLane =\n typeof row.workspaceId === \"string\" && row.workspaceId.length > 0;\n const newSk = isWorkspaceLane\n ? buildRoleAssignmentUserProjectionSkWorkspaceLane({\n tenantId: row.tenantId,\n workspaceId: row.workspaceId as string,\n roleId: row.roleId,\n roleAssignmentId: row.roleAssignmentId,\n denormalizedRoleName: newName,\n })\n : buildRoleAssignmentUserProjectionSkTenantLane({\n tenantId: row.tenantId,\n roleId: row.roleId,\n roleAssignmentId: row.roleAssignmentId,\n denormalizedRoleName: newName,\n });\n targets.push({\n entity: \"roleAssignmentUserProjection\",\n oldKey: { userId: row.userId, sk: row.sk },\n newKey: { userId: row.userId, sk: newSk },\n newItem: {\n ...row,\n sk: newSk,\n denormalizedRoleName: newName,\n },\n skRewriteRequired: row.sk !== newSk,\n });\n }\n}\n\n/**\n * Tenant rename — only the `denormalizedTenantName` attr updates on\n * affected rows. SKs never carry tenant-name in the OpenHI grammar (the\n * Membership user-projection tenant-lane SK encodes `<normalizedTenantName>`\n * — see pattern #3 — so that single sub-lane DOES need an SK rewrite).\n *\n * Discovery: page canonical Memberships for this tenant via GSI1, then\n * for each affected user enumerate their pattern-#3 user-projection rows\n * (those that key off `<normalizedTenantName>`).\n */\nasync function pageTenantRename(params: {\n readonly tenantId: string;\n readonly oldName: string;\n readonly newName: string;\n readonly cursors: RenameCascadeCursorMap;\n readonly limit: number;\n readonly tableName?: string;\n}): Promise<ListRenameCascadeTargetsResult> {\n const { tenantId, newName, cursors, limit, tableName } = params;\n const service = getDynamoControlService(tableName);\n const nextCursors: RenameCascadeCursorMap = {};\n const targets: Array<RenameCascadeRewriteTarget> = [];\n\n // Discovery — page canonical Memberships for this tenant via GSI1.\n // GSI1 is sharded; for #1023 v1 we walk shard 0 only. Multi-shard\n // discovery is a tight follow-up — it requires either iterating\n // shards in a fan-out (4 queries per page) or threading a per-shard\n // cursor map. The current implementation accepts coverage limited\n // to shard 0; large tenants will need the follow-up.\n const discoveryCursor = cursors.tenantDiscovery;\n if (discoveryCursor !== null) {\n const page = await service.entities.membership.query\n .gsi1({ tenantId, gsi1Shard: \"0\" })\n .go({ cursor: discoveryCursor ?? null, limit });\n\n for (const row of page.data ?? []) {\n const userId = extractUserIdFromResource(row.resource);\n if (userId === undefined) {\n continue;\n }\n // Per affected user, rewrite the pattern-#3 user-projection row\n // for this tenant. The SK is\n // `MEMBERSHIP#TENANT#<normalizedTenantName>#TID#<tenantId>#<id>`.\n const userPage = await service.entities.membershipUserProjection.query\n .record({ userId })\n .begins({ sk: `MEMBERSHIP#TENANT#` })\n .where((attr, op) => op.eq(attr.tenantId, tenantId))\n .go({});\n for (const userRow of userPage.data ?? []) {\n const newSk = buildMembershipUserProjectionSkTenantLane({\n tenantId: userRow.tenantId,\n membershipId: userRow.membershipId,\n denormalizedTenantName: newName,\n });\n targets.push({\n entity: \"membershipUserProjection\",\n oldKey: { userId: userRow.userId, sk: userRow.sk },\n newKey: { userId: userRow.userId, sk: newSk },\n newItem: {\n ...userRow,\n sk: newSk,\n denormalizedTenantName: newName,\n },\n skRewriteRequired: userRow.sk !== newSk,\n });\n }\n // Pattern #4 workspace-lane user-projection rows carry\n // `denormalizedTenantName` as an attr only — no SK rewrite.\n const wsPage = await service.entities.membershipUserProjection.query\n .record({ userId })\n .begins({ sk: `MEMBERSHIP#WORKSPACE#TID#${tenantId}#` })\n .go({});\n for (const wsRow of wsPage.data ?? []) {\n // SK stays the same — pattern #4 encodes tenant by raw\n // `<tenantId>`, not name.\n const newSk = buildMembershipUserProjectionSkWorkspaceLane({\n tenantId: wsRow.tenantId,\n workspaceId: wsRow.workspaceId as string,\n membershipId: wsRow.membershipId,\n denormalizedWorkspaceName: wsRow.denormalizedWorkspaceName,\n });\n targets.push({\n entity: \"membershipUserProjection\",\n oldKey: { userId: wsRow.userId, sk: wsRow.sk },\n newKey: { userId: wsRow.userId, sk: newSk },\n newItem: {\n ...wsRow,\n sk: newSk,\n denormalizedTenantName: newName,\n },\n skRewriteRequired: wsRow.sk !== newSk,\n });\n }\n }\n nextCursors.tenantDiscovery = page.cursor ?? null;\n } else {\n nextCursors.tenantDiscovery = null;\n }\n nextCursors.membershipUserProjection = null;\n nextCursors.roleAssignmentUserProjection = null;\n\n const exhausted = nextCursors.tenantDiscovery === null;\n\n return { targets, cursors: nextCursors, exhausted };\n}\n\n/**\n * Extract `userId` from a canonical Membership / RoleAssignment resource\n * JSON string. The canonical row stores the user reference inside the\n * resource (FHIR `Reference` shape — `\\{ \"user\": \\{ \"reference\": \"User/<id>\" \\} \\}`);\n * discovery via GSI1 returns the canonical row, and the cascade needs\n * `userId` to address the user partition for projection rewrites.\n * Returns `undefined` when the field is missing or malformed so the\n * cascade can skip rows it cannot resolve.\n */\nfunction extractUserIdFromResource(resource: unknown): string | undefined {\n if (typeof resource !== \"string\" || resource.length === 0) {\n return undefined;\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(resource);\n } catch {\n return undefined;\n }\n if (!parsed || typeof parsed !== \"object\") {\n return undefined;\n }\n return extractReferenceSlug(parsed as Record<string, unknown>, \"user\");\n}\n"],"mappings":"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","names":[]}
@@ -1468,6 +1468,24 @@ var TenantEntity = new import_electrodb11.Entity({
1468
1468
  type: "string",
1469
1469
  required: true
1470
1470
  },
1471
+ /**
1472
+ * ADR-028 denormalized counter — number of tenant-scoped Memberships
1473
+ * (users) in this tenant. Maintained by the counter-maintenance
1474
+ * consumer via atomic ADD; absent/0 until first event or reconciliation.
1475
+ */
1476
+ usersInTenant: {
1477
+ type: "number",
1478
+ required: false
1479
+ },
1480
+ /**
1481
+ * ADR-028 denormalized counter — number of Workspaces in this tenant.
1482
+ * Maintained by the counter-maintenance consumer via atomic ADD;
1483
+ * absent/0 until first event or reconciliation.
1484
+ */
1485
+ workspacesInTenant: {
1486
+ type: "number",
1487
+ required: false
1488
+ },
1471
1489
  gsi1Shard: gsi1ShardAttribute,
1472
1490
  /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
1473
1491
  gsi1sk: gsi1skAttribute,
@@ -1572,6 +1590,26 @@ var UserEntity = new import_electrodb12.Entity({
1572
1590
  type: "string",
1573
1591
  required: true
1574
1592
  },
1593
+ /**
1594
+ * ADR-028 denormalized counter — number of tenant-scoped Memberships
1595
+ * (tenants) this user belongs to. Maintained by the
1596
+ * counter-maintenance consumer via atomic ADD; absent/0 until first
1597
+ * event or reconciliation.
1598
+ */
1599
+ tenantsForUser: {
1600
+ type: "number",
1601
+ required: false
1602
+ },
1603
+ /**
1604
+ * ADR-028 denormalized counter — number of workspace-scoped
1605
+ * Memberships (workspaces) this user belongs to. Maintained by the
1606
+ * counter-maintenance consumer via atomic ADD; absent/0 until first
1607
+ * event or reconciliation.
1608
+ */
1609
+ workspacesForUser: {
1610
+ type: "number",
1611
+ required: false
1612
+ },
1575
1613
  gsi1Shard: gsi1ShardAttribute,
1576
1614
  /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
1577
1615
  gsi1sk: gsi1skAttribute,
@@ -1716,6 +1754,36 @@ var WorkspaceEntity = new import_electrodb13.Entity({
1716
1754
  type: "string",
1717
1755
  required: true
1718
1756
  },
1757
+ /**
1758
+ * ADR-028 denormalized counter — number of workspace-scoped
1759
+ * Memberships (users) in this workspace. Maintained by the
1760
+ * counter-maintenance consumer via atomic ADD; absent/0 until first
1761
+ * event or reconciliation.
1762
+ */
1763
+ usersInWorkspace: {
1764
+ type: "number",
1765
+ required: false
1766
+ },
1767
+ /**
1768
+ * ADR-028 denormalized counter — number of workspace-scoped
1769
+ * RoleAssignments classified as admin-tier in this workspace.
1770
+ * Maintained by the counter-maintenance consumer via atomic ADD;
1771
+ * absent/0 until first event or reconciliation.
1772
+ */
1773
+ adminUsersInWorkspace: {
1774
+ type: "number",
1775
+ required: false
1776
+ },
1777
+ /**
1778
+ * ADR-028 denormalized counter — number of workspace-scoped
1779
+ * RoleAssignments classified as non-admin in this workspace.
1780
+ * Maintained by the counter-maintenance consumer via atomic ADD;
1781
+ * absent/0 until first event or reconciliation.
1782
+ */
1783
+ normalUsersInWorkspace: {
1784
+ type: "number",
1785
+ required: false
1786
+ },
1719
1787
  gsi1Shard: gsi1ShardAttribute,
1720
1788
  /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
1721
1789
  gsi1sk: gsi1skAttribute,