@openhi/constructs 0.0.127 → 0.0.128
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.
- package/lib/{chunk-ZHMHLK3S.mjs → chunk-7BQHLC7U.mjs} +3 -3
- package/lib/{chunk-7WDX6GPO.mjs → chunk-AWYZJFPL.mjs} +3 -3
- package/lib/{chunk-AHYQFT4N.mjs → chunk-E6MCKJVS.mjs} +3 -3
- package/lib/{chunk-W4KR4CSL.mjs → chunk-M7Y3BOQW.mjs} +2 -2
- package/lib/chunk-M7Y3BOQW.mjs.map +1 -0
- package/lib/{chunk-U7L7T4XU.mjs → chunk-MVQWAIMC.mjs} +2 -2
- package/lib/{chunk-U7L7T4XU.mjs.map → chunk-MVQWAIMC.mjs.map} +1 -1
- package/lib/{chunk-VXX4I3EF.mjs → chunk-SVXLZCF7.mjs} +1 -1
- package/lib/{chunk-VXX4I3EF.mjs.map → chunk-SVXLZCF7.mjs.map} +1 -1
- package/lib/{chunk-YYRWDEG4.mjs → chunk-UT7ADSEJ.mjs} +3 -3
- package/lib/{chunk-YYRWDEG4.mjs.map → chunk-UT7ADSEJ.mjs.map} +1 -1
- package/lib/{chunk-6NBGYGFL.mjs → chunk-VZCPGQXA.mjs} +13 -12
- package/lib/chunk-VZCPGQXA.mjs.map +1 -0
- package/lib/{chunk-WXS3PUHR.mjs → chunk-WGA43MMY.mjs} +4 -4
- package/lib/{chunk-WQWFVEVX.mjs → chunk-X5E4YJGZ.mjs} +2 -2
- package/lib/{chunk-KO64HPWQ.mjs → chunk-ZWSGM6PZ.mjs} +2 -2
- package/lib/delete-chunk.handler.d.mts +1 -1
- package/lib/delete-chunk.handler.d.ts +1 -1
- package/lib/delete-chunk.handler.js +12 -11
- package/lib/delete-chunk.handler.js.map +1 -1
- package/lib/delete-chunk.handler.mjs +2 -2
- package/lib/delete-chunk.handler.mjs.map +1 -1
- package/lib/{events-BfrkMoBD.d.mts → events-COI0BuMM.d.mts} +1 -1
- package/lib/{events-BfrkMoBD.d.ts → events-COI0BuMM.d.ts} +1 -1
- package/lib/index.d.mts +8 -10
- package/lib/index.d.ts +8 -10
- package/lib/index.js +14 -14
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +8 -9
- package/lib/index.mjs.map +1 -1
- package/lib/list-chunks.handler.d.mts +1 -1
- package/lib/list-chunks.handler.d.ts +1 -1
- package/lib/list-chunks.handler.js +12 -11
- package/lib/list-chunks.handler.js.map +1 -1
- package/lib/list-chunks.handler.mjs +2 -2
- package/lib/list-chunks.handler.mjs.map +1 -1
- package/lib/platform-deploy-bridge.handler.d.mts +1 -1
- package/lib/platform-deploy-bridge.handler.d.ts +1 -1
- package/lib/platform-deploy-bridge.handler.js.map +1 -1
- package/lib/platform-deploy-bridge.handler.mjs +1 -1
- package/lib/pre-token-generation.handler.js +12 -11
- package/lib/pre-token-generation.handler.js.map +1 -1
- package/lib/pre-token-generation.handler.mjs +4 -4
- package/lib/provision-default-workspace.handler.js +12 -11
- package/lib/provision-default-workspace.handler.js.map +1 -1
- package/lib/provision-default-workspace.handler.mjs +4 -4
- package/lib/rename-list-targets.handler.d.mts +1 -1
- package/lib/rename-list-targets.handler.d.ts +1 -1
- package/lib/rename-list-targets.handler.js +12 -11
- package/lib/rename-list-targets.handler.js.map +1 -1
- package/lib/rename-list-targets.handler.mjs +2 -2
- package/lib/rename-list-targets.handler.mjs.map +1 -1
- package/lib/rename-rewrite-chunk.handler.d.mts +1 -1
- package/lib/rename-rewrite-chunk.handler.d.ts +1 -1
- package/lib/rename-rewrite-chunk.handler.js +12 -11
- package/lib/rename-rewrite-chunk.handler.js.map +1 -1
- package/lib/rename-rewrite-chunk.handler.mjs +2 -2
- package/lib/rename-rewrite-chunk.handler.mjs.map +1 -1
- package/lib/rest-api-lambda.handler.js +12 -11
- package/lib/rest-api-lambda.handler.js.map +1 -1
- package/lib/rest-api-lambda.handler.mjs +7 -7
- package/lib/rest-api-lambda.handler.mjs.map +1 -1
- package/lib/seed-demo-data.handler.js +12 -11
- package/lib/seed-demo-data.handler.js.map +1 -1
- package/lib/seed-demo-data.handler.mjs +5 -5
- package/lib/seed-system-data.handler.js +12 -11
- package/lib/seed-system-data.handler.js.map +1 -1
- package/lib/seed-system-data.handler.mjs +2 -2
- package/lib/static-hosting.viewer-request-handler.d.mts +1 -1
- package/lib/static-hosting.viewer-request-handler.d.ts +1 -1
- package/lib/static-hosting.viewer-request-handler.js.map +1 -1
- package/lib/static-hosting.viewer-request-handler.mjs.map +1 -1
- package/package.json +5 -5
- package/lib/chunk-6NBGYGFL.mjs.map +0 -1
- package/lib/chunk-W4KR4CSL.mjs.map +0 -1
- /package/lib/{chunk-ZHMHLK3S.mjs.map → chunk-7BQHLC7U.mjs.map} +0 -0
- /package/lib/{chunk-7WDX6GPO.mjs.map → chunk-AWYZJFPL.mjs.map} +0 -0
- /package/lib/{chunk-AHYQFT4N.mjs.map → chunk-E6MCKJVS.mjs.map} +0 -0
- /package/lib/{chunk-WXS3PUHR.mjs.map → chunk-WGA43MMY.mjs.map} +0 -0
- /package/lib/{chunk-WQWFVEVX.mjs.map → chunk-X5E4YJGZ.mjs.map} +0 -0
- /package/lib/{chunk-KO64HPWQ.mjs.map → chunk-ZWSGM6PZ.mjs.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/workflows/control-plane/platform-deploy-bridge/events.ts"],"sourcesContent":["/**\n * @see sites/www-docs/content/packages/@openhi/constructs/workflows/control-plane/platform-deploy-bridge/index.md\n */\n\n/** EventBridge `source` for the AWS-native CloudFormation events the bridge listens to. */\nexport const CLOUDFORMATION_EVENT_SOURCE = \"aws.cloudformation\" as const;\n\n/** EventBridge `detail-type` for terminal stack status events. */\nexport const CLOUDFORMATION_STACK_STATUS_CHANGE_DETAIL_TYPE =\n \"CloudFormation Stack Status Change\" as const;\n\n/** Stack statuses the bridge republishes. Other statuses are pre-filtered at the rule. */\nexport const BRIDGED_STATUSES = [\"CREATE_COMPLETE\", \"UPDATE_COMPLETE\"] as const;\nexport type BridgedStatus = (typeof BRIDGED_STATUSES)[number];\n\n/** Env var the bridge handler reads to discover the control event bus name. */\nexport const CONTROL_EVENT_BUS_NAME_ENV_VAR = \"CONTROL_EVENT_BUS_NAME\";\n\n/**\n * Env var the bridge handler reads for the resolved\n * `openhi:repo-name`-shaped tag key. Resolved at synth time from the host\n * stack's `appName` + {@link OPENHI_TAG_SUFFIX_REPO_NAME}.\n */\nexport const OPENHI_REPO_TAG_KEY_ENV_VAR = \"OPENHI_REPO_TAG_KEY\";\n\n/**\n * Env var the bridge handler reads for the resolved `openhi:` tag prefix.\n * Resolved at synth time from the host stack's `appName`. Used to project\n * the relevant stack tags into the published envelope.\n */\nexport const OPENHI_TAG_KEY_PREFIX_ENV_VAR = \"OPENHI_TAG_KEY_PREFIX\";\n\n/** Free-form `actor.system` value per TR-016 § Decision points #1 (bootstrap-role pattern). */\nexport const PLATFORM_DEPLOY_BRIDGE_ACTOR_SYSTEM = \"platform-deploy-bridge\";\n\n/**\n * Subset of the CloudFormation Stack Status Change `detail` field the\n * bridge handler reads. AWS-side schema lives at\n *
|
|
1
|
+
{"version":3,"sources":["../src/workflows/control-plane/platform-deploy-bridge/events.ts"],"sourcesContent":["/**\n * @see sites/www-docs/content/packages/@openhi/constructs/workflows/control-plane/platform-deploy-bridge/index.md\n */\n\n/** EventBridge `source` for the AWS-native CloudFormation events the bridge listens to. */\nexport const CLOUDFORMATION_EVENT_SOURCE = \"aws.cloudformation\" as const;\n\n/** EventBridge `detail-type` for terminal stack status events. */\nexport const CLOUDFORMATION_STACK_STATUS_CHANGE_DETAIL_TYPE =\n \"CloudFormation Stack Status Change\" as const;\n\n/** Stack statuses the bridge republishes. Other statuses are pre-filtered at the rule. */\nexport const BRIDGED_STATUSES = [\"CREATE_COMPLETE\", \"UPDATE_COMPLETE\"] as const;\nexport type BridgedStatus = (typeof BRIDGED_STATUSES)[number];\n\n/** Env var the bridge handler reads to discover the control event bus name. */\nexport const CONTROL_EVENT_BUS_NAME_ENV_VAR = \"CONTROL_EVENT_BUS_NAME\";\n\n/**\n * Env var the bridge handler reads for the resolved\n * `openhi:repo-name`-shaped tag key. Resolved at synth time from the host\n * stack's `appName` + {@link OPENHI_TAG_SUFFIX_REPO_NAME}.\n */\nexport const OPENHI_REPO_TAG_KEY_ENV_VAR = \"OPENHI_REPO_TAG_KEY\";\n\n/**\n * Env var the bridge handler reads for the resolved `openhi:` tag prefix.\n * Resolved at synth time from the host stack's `appName`. Used to project\n * the relevant stack tags into the published envelope.\n */\nexport const OPENHI_TAG_KEY_PREFIX_ENV_VAR = \"OPENHI_TAG_KEY_PREFIX\";\n\n/** Free-form `actor.system` value per TR-016 § Decision points #1 (bootstrap-role pattern). */\nexport const PLATFORM_DEPLOY_BRIDGE_ACTOR_SYSTEM = \"platform-deploy-bridge\";\n\n/**\n * Subset of the CloudFormation Stack Status Change `detail` field the\n * bridge handler reads. AWS-side schema lives at\n * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/monitoring-cloudformation.html .\n */\nexport interface CloudFormationStackStatusChangeDetail {\n readonly \"stack-id\": string;\n readonly \"logical-resource-id\"?: string;\n readonly \"physical-resource-id\"?: string;\n readonly \"status-details\": {\n readonly status: string;\n readonly \"status-reason\"?: string;\n };\n readonly \"resource-type\"?: string;\n readonly \"client-request-token\"?: string;\n}\n"],"mappings":";AAKO,IAAM,8BAA8B;AAGpC,IAAM,iDACX;AAGK,IAAM,mBAAmB,CAAC,mBAAmB,iBAAiB;AAI9D,IAAM,iCAAiC;AAOvC,IAAM,8BAA8B;AAOpC,IAAM,gCAAgC;AAGtC,IAAM,sCAAsC;","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
batchGetWithRetry,
|
|
3
3
|
dispatchListMode
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-MVQWAIMC.mjs";
|
|
5
5
|
import {
|
|
6
6
|
ForbiddenError,
|
|
7
7
|
NotFoundError,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import {
|
|
11
11
|
SHARD_COUNT,
|
|
12
12
|
getDynamoControlService
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-VZCPGQXA.mjs";
|
|
14
14
|
|
|
15
15
|
// src/data/operations/control/user/user-find-by-sub-operation.ts
|
|
16
16
|
async function findUserBySubOperation(params) {
|
|
@@ -304,4 +304,4 @@ export {
|
|
|
304
304
|
idFromReference,
|
|
305
305
|
switchUserTenantWorkspaceOperation
|
|
306
306
|
};
|
|
307
|
-
//# sourceMappingURL=chunk-
|
|
307
|
+
//# sourceMappingURL=chunk-UT7ADSEJ.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/data/operations/control/user/user-find-by-sub-operation.ts","../src/data/operations/control/user/user-resource-helpers.ts","../src/data/operations/fhir-reference.ts","../src/data/operations/control/user/user-create-operation.ts","../src/data/operations/control/user/user-get-by-id-operation.ts","../src/data/operations/control/user/user-list-operation.ts","../src/data/operations/control/user/user-switch-tenant-workspace-operation.ts","../src/data/operations/control/membership/membership-list-by-user-operation.ts","../src/data/operations/control/user/user-update-operation.ts","../src/data/operations/control/user/user-delete-operation.ts"],"sourcesContent":["import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { OpenHiContext } from \"../../../openhi-context\";\n\nexport interface FindUserBySubParams {\n context: OpenHiContext;\n cognitoSub: string;\n tableName?: string;\n}\n\nexport interface FindUserBySubResult {\n id: string;\n cognitoSub?: string;\n resource: string;\n vid: string;\n}\n\n/**\n * Look up a User by Cognito sub via GSI2, projecting the row to a stable\n * result shape. Returns `undefined` when no row matches.\n */\nexport async function findUserBySubOperation(\n params: FindUserBySubParams,\n): Promise<FindUserBySubResult | undefined> {\n const { cognitoSub, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const result = await service.entities.user.query\n .gsi2({ cognitoSub })\n .go({ limit: 1 });\n const item = result.data?.[0];\n if (!item) {\n return undefined;\n }\n return {\n id: item.id,\n cognitoSub: item.cognitoSub,\n resource: item.resource,\n vid: item.vid,\n };\n}\n","import type { User } from \"@openhi/types\";\n\n/**\n * Helpers for working with persisted OpenHI User resources. Co-located with\n * the User operations because both the Cognito triggers and the onboarding\n * workflow consume these alongside `findUserBySubOperation`.\n */\n\n// Defensive parse — JSON.parse may yield any shape, so every field is optional.\nexport type UserResource = Partial<User>;\n\n/**\n * Existing User resources are stored as JSON strings in the data store; parse\n * defensively so a malformed payload returns `undefined` rather than throwing.\n */\nexport function parseUserResource(resource: string): UserResource | undefined {\n try {\n return JSON.parse(resource) as UserResource;\n } catch {\n return undefined;\n }\n}\n","/**\n * Pure helpers for working with FHIR Reference fields. Shared across data-plane\n * and control-plane operations and the handlers that wrap them.\n */\n\n/**\n * Extract the id portion from a FHIR-style reference such as `Patient/<id>` or\n * `Tenant/<id>`. Returns `undefined` if the reference is missing, does not\n * match the prefix, or has an empty id after the prefix.\n */\nexport function idFromReference(\n reference: string | undefined,\n prefix: string,\n): string | undefined {\n if (!reference || !reference.startsWith(prefix)) {\n return undefined;\n }\n const id = reference.slice(prefix.length);\n return id.length > 0 ? id : undefined;\n}\n","import { extractSummary, type FhirResourceLike } from \"@openhi/types\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { OpenHiContext } from \"../../../openhi-context\";\n\nexport interface UserCreateParams {\n context: OpenHiContext;\n body: { id?: string; resource?: Record<string, unknown> | string };\n tableName?: string;\n}\n\nexport interface UserCreateResult {\n id: string;\n resource: { resourceType: string; id: string; [key: string]: unknown };\n meta: { lastUpdated: string; versionId: string };\n}\n\nexport async function createUserOperation(\n params: UserCreateParams,\n): Promise<UserCreateResult> {\n const { context, body, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const id = body.id ?? `user-${Date.now()}`;\n const parsedResource =\n typeof body.resource === \"string\"\n ? (JSON.parse(body.resource) as Record<string, unknown>)\n : (body.resource ?? {});\n\n const lastUpdated = context.date ?? new Date().toISOString();\n const vid = `1`;\n\n const resource = { resourceType: \"User\", id, ...parsedResource };\n const summary = JSON.stringify(extractSummary(resource as FhirResourceLike));\n\n await service.entities.user\n .put({\n id,\n resource: JSON.stringify(resource),\n summary,\n vid,\n lastUpdated,\n })\n .go();\n\n return {\n id,\n resource,\n meta: { lastUpdated, versionId: vid },\n };\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { NotFoundError } from \"../../../errors\";\nimport { OpenHiContext } from \"../../../openhi-context\";\n\nexport interface UserGetByIdParams {\n context: OpenHiContext;\n id: string;\n tableName?: string;\n}\n\nexport interface UserGetByIdResult {\n id: string;\n resource: { resourceType: string; id: string; [key: string]: unknown };\n}\n\nexport async function getUserByIdOperation(\n params: UserGetByIdParams,\n): Promise<UserGetByIdResult> {\n const { id, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const response = await service.entities.user.get({ id, sk: \"CURRENT\" }).go();\n\n const item = response.data;\n if (!item) {\n throw new NotFoundError(`User not found: ${id}`);\n }\n\n const parsedResource = JSON.parse(item.resource) as Record<string, unknown>;\n\n return {\n id,\n resource: { resourceType: \"User\", id, ...parsedResource },\n };\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { SHARD_COUNT } from \"../../../dynamo/shard\";\nimport { OpenHiContext } from \"../../../openhi-context\";\nimport {\n batchGetWithRetry,\n dispatchListMode,\n type ListOperationMode,\n} from \"../../data-operations-common\";\n\nconst SK = \"CURRENT\";\n\nexport interface UserListParams {\n context: OpenHiContext;\n tableName?: string;\n /** #853: defaults to `\"full\"`. `\"summary\"` skips BatchGet, `\"count\"` returns total only. */\n mode?: ListOperationMode;\n}\n\nexport interface UserListResult {\n entries: Array<{\n id: string;\n resource: { resourceType: string; id: string; [key: string]: unknown };\n }>;\n total: number;\n}\n\n/**\n * Lists Users via GSI1 (sharded). `mode` (default `\"full\"`) selects between BatchGet hydration,\n * summary-only (parse `summary` JSON projected on GSI1), or count-only (skip both). See\n * `dispatchListMode` in data-operations-common for the canonical mode contract.\n */\nexport async function listUsersOperation(\n params: UserListParams,\n): Promise<UserListResult> {\n const { tableName, mode = \"full\" } = params;\n const service = getDynamoControlService(tableName);\n\n const shardResults = await Promise.all(\n Array.from({ length: SHARD_COUNT }, (_, shard) =>\n service.entities.user.query.gsi1({ gsi1Shard: String(shard) }).go(),\n ),\n );\n\n return dispatchListMode<\n { id: string; resource: string },\n UserListResult[\"entries\"][number]\n >(mode, shardResults, {\n hydrate: (orderedIds) =>\n batchGetWithRetry(\n service.entities.user,\n orderedIds.map((id) => ({ id, sk: SK })),\n ) as Promise<Array<{ id: string; resource: string }>>,\n getId: (item) => item.id,\n buildEntry: (id, item) => ({\n id,\n resource: {\n resourceType: \"User\",\n id,\n ...(JSON.parse(item.resource) as Record<string, unknown>),\n },\n }),\n buildSummaryEntry: (id, parsed) => ({\n id,\n resource: { resourceType: \"User\", id, ...parsed },\n }),\n });\n}\n","import { extractSummary, type FhirResourceLike } from \"@openhi/types\";\nimport { findUserBySubOperation } from \"./user-find-by-sub-operation\";\nimport { parseUserResource } from \"./user-resource-helpers\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport {\n ForbiddenError,\n NotFoundError,\n ValidationError,\n} from \"../../../errors\";\nimport { idFromReference } from \"../../fhir-reference\";\nimport { membershipListByUserOperation } from \"../membership/membership-list-by-user-operation\";\n\nconst SK = \"CURRENT\";\n\nexport interface UserSwitchTenantWorkspaceParams {\n cognitoSub: string;\n tenantReference: string;\n workspaceReference: string;\n tableName?: string;\n /** Override the clock — used by tests for deterministic `lastUpdated`. */\n now?: () => Date;\n}\n\nexport interface UserSwitchTenantWorkspaceResult {\n id: string;\n resource: Record<string, unknown>;\n meta: { lastUpdated: string; versionId: string };\n}\n\n/**\n * Update `currentTenant` and `currentWorkspace` on the User resource for the\n * caller authenticated by the given Cognito `sub`. All other fields on the\n * User are preserved.\n *\n * Membership pre-condition uses the ADR-018 adjacency-list user projection\n * (pattern #4, workspace sub-lane). A single base-table Query on\n * `PK = USER#ID#<userId>` with `SK begins_with\n * 'MEMBERSHIP#WORKSPACE#TID#<tenantId>#'` (via\n * {@link membershipListByUserOperation} with `mode: \"workspaceInTenant\"`)\n * confirms the caller has a workspace-level Membership in the requested\n * tenant + workspace pair. No GSI1 fan-out, no scan.\n *\n * Throws:\n * - `ValidationError` when either reference is missing or malformed\n * - `NotFoundError` when no User matches the Cognito subject\n * - `ForbiddenError` when the caller has no Membership in the requested\n * `(tenantId, workspaceId)` pair on their user-partition projection\n *\n * @see https://github.com/codedrifters/openhi/issues/769\n * @see https://github.com/codedrifters/openhi/issues/1020\n * @see ADR-018 § Access Pattern Coverage (pattern #4)\n */\nexport async function switchUserTenantWorkspaceOperation(\n params: UserSwitchTenantWorkspaceParams,\n): Promise<UserSwitchTenantWorkspaceResult> {\n const { cognitoSub, tenantReference, workspaceReference, tableName } = params;\n\n const tenantId = idFromReference(tenantReference, \"Tenant/\");\n if (!tenantId) {\n throw new ValidationError(\n \"tenant.reference must be a 'Tenant/<id>' reference.\",\n );\n }\n const workspaceId = idFromReference(workspaceReference, \"Workspace/\");\n if (!workspaceId) {\n throw new ValidationError(\n \"workspace.reference must be a 'Workspace/<id>' reference.\",\n );\n }\n\n const user = await findUserBySubOperation({\n // findUserBySubOperation does not read context fields; pass a stub.\n context: {\n tenantId: \"\",\n workspaceId: \"\",\n date: \"\",\n actorId: \"\",\n actorName: \"\",\n actorType: \"internal-system\",\n },\n cognitoSub,\n tableName,\n });\n if (!user) {\n throw new NotFoundError(\n \"User not yet provisioned for the authenticated Cognito subject.\",\n );\n }\n\n // ADR-018: single Query on the user partition, narrowed to the workspace\n // sub-lane of the requested tenant. The lane includes `workspaceId` on\n // every row; a row with `workspaceId === <requested>` is sufficient and\n // necessary proof that the caller may switch to that pair.\n const projection = await membershipListByUserOperation({\n userId: user.id,\n mode: \"workspaceInTenant\",\n tenantId,\n tableName,\n });\n const hasMembership = projection.items.some(\n (row) => row.workspaceId === workspaceId,\n );\n if (!hasMembership) {\n throw new ForbiddenError(\n `User is not a member of Workspace/${workspaceId} in Tenant/${tenantId}.`,\n );\n }\n\n const existingResource = parseUserResource(user.resource) ?? {};\n const updatedResource: Record<string, unknown> = {\n ...existingResource,\n resourceType: \"User\",\n id: user.id,\n currentTenant: { reference: `Tenant/${tenantId}` },\n currentWorkspace: { reference: `Workspace/${workspaceId}` },\n };\n\n const lastUpdated = (params.now ? params.now() : new Date()).toISOString();\n const vid = `${Date.now()}`;\n const summary = JSON.stringify(\n extractSummary(updatedResource as FhirResourceLike),\n );\n\n const service = getDynamoControlService(tableName);\n await service.entities.user\n .patch({ id: user.id, sk: SK })\n .set({\n resource: JSON.stringify(updatedResource),\n summary,\n vid,\n lastUpdated,\n })\n .go();\n\n return {\n id: user.id,\n resource: updatedResource,\n meta: { lastUpdated, versionId: vid },\n };\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\n\n/**\n * Filter modes for {@link membershipListByUserOperation}.\n *\n * Maps directly to the ADR-018 sub-lane discriminator in the user-projection\n * SK (`MEMBERSHIP#TENANT#…` vs `MEMBERSHIP#WORKSPACE#…`):\n *\n * - `\"all\"` — `Query(PK = USER#ID#<userId>, SK begins_with 'MEMBERSHIP#')`.\n * Returns both lanes interleaved in raw SK order.\n * - `\"tenant\"` — `SK begins_with 'MEMBERSHIP#TENANT#'`. Pattern #3 only.\n * - `\"workspace\"` — `SK begins_with 'MEMBERSHIP#WORKSPACE#'`. Pattern #4\n * across every tenant.\n * - `\"workspaceInTenant\"` — `SK begins_with 'MEMBERSHIP#WORKSPACE#TID#<tenantId>#'`.\n * Pattern #4 narrowed to one tenant. Requires `tenantId`.\n */\nexport type MembershipListByUserMode =\n | \"all\"\n | \"tenant\"\n | \"workspace\"\n | \"workspaceInTenant\";\n\n/** Inputs accepted by {@link membershipListByUserOperation}. */\nexport interface MembershipListByUserParams {\n readonly userId: string;\n /** Filter mode — see {@link MembershipListByUserMode}. Defaults to `\"all\"`. */\n readonly mode?: MembershipListByUserMode;\n /** Required only when `mode === \"workspaceInTenant\"`. */\n readonly tenantId?: string;\n /** ElectroDB cursor from a prior page. Forwarded to `.go({ cursor })`. */\n readonly cursor?: string | null;\n /** Per-page item limit forwarded to `.go({ limit })`. */\n readonly limit?: number;\n /** Sort order forwarded to `.go({ order })`. Defaults to ElectroDB's `\"asc\"`. */\n readonly order?: \"asc\" | \"desc\";\n /** Optional table-name override; resolved via env when omitted. */\n readonly tableName?: string;\n}\n\n/** One projection-row payload as returned to a consumer. */\nexport interface MembershipUserProjectionEntry {\n readonly userId: string;\n readonly sk: string;\n readonly tenantId: string;\n readonly workspaceId?: string;\n readonly membershipId: string;\n readonly summary: string;\n readonly vid: string;\n readonly lastUpdated: string;\n readonly denormalizedTenantName?: string;\n readonly denormalizedUserName?: string;\n readonly denormalizedWorkspaceName?: string;\n}\n\n/** Page returned by {@link membershipListByUserOperation}. */\nexport interface MembershipListByUserResult {\n readonly items: Array<MembershipUserProjectionEntry>;\n /** ElectroDB cursor for the next page, or `null` when exhausted. */\n readonly cursor: string | null;\n}\n\n/**\n * Compose the SK prefix for a given filter mode. Centralizing the\n * prefix string here keeps the SK grammar (owned by\n * `membership-user-projection.ts`) the single source of truth for the\n * lane discriminators — this function reads them, it does not invent them.\n */\nfunction buildSkPrefix(\n mode: MembershipListByUserMode,\n tenantId: string | undefined,\n): string {\n switch (mode) {\n case \"tenant\":\n return \"MEMBERSHIP#TENANT#\";\n case \"workspace\":\n return \"MEMBERSHIP#WORKSPACE#\";\n case \"workspaceInTenant\":\n // Pattern-#4 SK places `<tenantId>` directly after the\n // `MEMBERSHIP#WORKSPACE#TID#` segment so a `begins_with` filter\n // narrows the workspace lane to a single tenant.\n return `MEMBERSHIP#WORKSPACE#TID#${tenantId}#`;\n case \"all\":\n default:\n return \"MEMBERSHIP#\";\n }\n}\n\n/**\n * List Memberships for a user via the ADR-018 user-partition projection\n * (no GSI hop).\n *\n * Reads `MembershipUserProjectionEntity` rows under `PK = USER#ID#<userId>`\n * with an `SK begins_with` filter selected by `mode`:\n *\n * | Mode | SK begins_with | Covers |\n * |---|---|---|\n * | `all` (default) | `MEMBERSHIP#` | patterns #3 + #4 interleaved |\n * | `tenant` | `MEMBERSHIP#TENANT#` | pattern #3 only |\n * | `workspace` | `MEMBERSHIP#WORKSPACE#` | pattern #4 only, across tenants |\n * | `workspaceInTenant` | `MEMBERSHIP#WORKSPACE#TID#<tenantId>#` | pattern #4 in one tenant |\n *\n * Returns the projection rows verbatim (`summary`, `vid`, `lastUpdated`\n * plus the projection-discriminating fields) — full canonical-resource\n * hydration is opt-in for callers via\n * `MembershipEntity.get({ tenantId, id: membershipId })`. Pagination\n * mirrors ElectroDB's native `.go({ cursor })` shape; the returned\n * `cursor` is opaque to callers.\n *\n * @see ADR-018 § Access Pattern Coverage (patterns #3 and #4)\n * @see .state/adr-018-implementation-guide.md § 1 (SK grammar)\n */\nexport async function membershipListByUserOperation(\n params: MembershipListByUserParams,\n): Promise<MembershipListByUserResult> {\n const {\n userId,\n mode = \"all\",\n tenantId,\n cursor = null,\n limit,\n order,\n tableName,\n } = params;\n\n if (mode === \"workspaceInTenant\" && !tenantId) {\n throw new Error(\n 'membershipListByUserOperation: tenantId is required when mode === \"workspaceInTenant\"',\n );\n }\n\n const service = getDynamoControlService(tableName);\n const skPrefix = buildSkPrefix(mode, tenantId);\n\n const goOptions: {\n cursor?: string | null;\n limit?: number;\n order?: \"asc\" | \"desc\";\n } = {\n cursor,\n };\n if (limit !== undefined) {\n goOptions.limit = limit;\n }\n if (order !== undefined) {\n goOptions.order = order;\n }\n\n const result = await service.entities.membershipUserProjection.query\n .record({ userId })\n .begins({ sk: skPrefix })\n .go(goOptions);\n\n const items: Array<MembershipUserProjectionEntry> = (result.data ?? []).map(\n (row) => ({\n userId: row.userId,\n sk: row.sk,\n tenantId: row.tenantId,\n workspaceId: row.workspaceId,\n membershipId: row.membershipId,\n summary: row.summary,\n vid: row.vid,\n lastUpdated: row.lastUpdated,\n denormalizedTenantName: row.denormalizedTenantName,\n denormalizedUserName: row.denormalizedUserName,\n denormalizedWorkspaceName: row.denormalizedWorkspaceName,\n }),\n );\n\n return { items, cursor: result.cursor ?? null };\n}\n","import { extractSummary, type FhirResourceLike } from \"@openhi/types\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { NotFoundError } from \"../../../errors\";\nimport { OpenHiContext } from \"../../../openhi-context\";\n\nexport interface UserUpdateParams {\n context: OpenHiContext;\n id: string;\n body: { resource?: Record<string, unknown> | string };\n tableName?: string;\n}\n\nexport interface UserUpdateResult {\n id: string;\n resource: { resourceType: string; id: string; [key: string]: unknown };\n meta: { lastUpdated: string; versionId: string };\n}\n\nexport async function updateUserOperation(\n params: UserUpdateParams,\n): Promise<UserUpdateResult> {\n const { context, id, body, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const existing = await service.entities.user.get({ id, sk: \"CURRENT\" }).go();\n if (!existing.data) {\n throw new NotFoundError(`User not found: ${id}`);\n }\n\n const parsedResource =\n typeof body.resource === \"string\"\n ? (JSON.parse(body.resource) as Record<string, unknown>)\n : (body.resource ?? {});\n\n const lastUpdated = context.date ?? new Date().toISOString();\n const vid = `${Date.now()}`;\n\n const resource = { resourceType: \"User\", id, ...parsedResource };\n const summary = JSON.stringify(extractSummary(resource as FhirResourceLike));\n\n await service.entities.user\n .put({\n id,\n resource: JSON.stringify(resource),\n summary,\n vid,\n lastUpdated,\n })\n .go();\n\n return {\n id,\n resource,\n meta: { lastUpdated, versionId: vid },\n };\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { OpenHiContext } from \"../../../openhi-context\";\n\nexport interface UserDeleteParams {\n context: OpenHiContext;\n id: string;\n tableName?: string;\n}\n\nexport async function deleteUserOperation(\n params: UserDeleteParams,\n): Promise<void> {\n const { id, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n await service.entities.user.delete({ id, sk: \"CURRENT\" }).go();\n}\n"],"mappings":"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","names":["extractSummary","SK","extractSummary","extractSummary","extractSummary"]}
|
|
1
|
+
{"version":3,"sources":["../src/data/operations/control/user/user-find-by-sub-operation.ts","../src/data/operations/control/user/user-resource-helpers.ts","../src/data/operations/fhir-reference.ts","../src/data/operations/control/user/user-create-operation.ts","../src/data/operations/control/user/user-get-by-id-operation.ts","../src/data/operations/control/user/user-list-operation.ts","../src/data/operations/control/user/user-switch-tenant-workspace-operation.ts","../src/data/operations/control/membership/membership-list-by-user-operation.ts","../src/data/operations/control/user/user-update-operation.ts","../src/data/operations/control/user/user-delete-operation.ts"],"sourcesContent":["import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { OpenHiContext } from \"../../../openhi-context\";\n\nexport interface FindUserBySubParams {\n context: OpenHiContext;\n cognitoSub: string;\n tableName?: string;\n}\n\nexport interface FindUserBySubResult {\n id: string;\n cognitoSub?: string;\n resource: string;\n vid: string;\n}\n\n/**\n * Look up a User by Cognito sub via GSI2, projecting the row to a stable\n * result shape. Returns `undefined` when no row matches.\n */\nexport async function findUserBySubOperation(\n params: FindUserBySubParams,\n): Promise<FindUserBySubResult | undefined> {\n const { cognitoSub, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const result = await service.entities.user.query\n .gsi2({ cognitoSub })\n .go({ limit: 1 });\n const item = result.data?.[0];\n if (!item) {\n return undefined;\n }\n return {\n id: item.id,\n cognitoSub: item.cognitoSub,\n resource: item.resource,\n vid: item.vid,\n };\n}\n","import type { User } from \"@openhi/types\";\n\n/**\n * Helpers for working with persisted OpenHI User resources. Co-located with\n * the User operations because both the Cognito triggers and the onboarding\n * workflow consume these alongside `findUserBySubOperation`.\n */\n\n// Defensive parse — JSON.parse may yield any shape, so every field is optional.\nexport type UserResource = Partial<User>;\n\n/**\n * Existing User resources are stored as JSON strings in the data store; parse\n * defensively so a malformed payload returns `undefined` rather than throwing.\n */\nexport function parseUserResource(resource: string): UserResource | undefined {\n try {\n return JSON.parse(resource) as UserResource;\n } catch {\n return undefined;\n }\n}\n","/**\n * Pure helpers for working with FHIR Reference fields. Shared across data-plane\n * and control-plane operations and the handlers that wrap them.\n */\n\n/**\n * Extract the id portion from a FHIR-style reference such as `Patient/<id>` or\n * `Tenant/<id>`. Returns `undefined` if the reference is missing, does not\n * match the prefix, or has an empty id after the prefix.\n */\nexport function idFromReference(\n reference: string | undefined,\n prefix: string,\n): string | undefined {\n if (!reference || !reference.startsWith(prefix)) {\n return undefined;\n }\n const id = reference.slice(prefix.length);\n return id.length > 0 ? id : undefined;\n}\n","import { extractSummary, type FhirResourceLike } from \"@openhi/types\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { OpenHiContext } from \"../../../openhi-context\";\n\nexport interface UserCreateParams {\n context: OpenHiContext;\n body: { id?: string; resource?: Record<string, unknown> | string };\n tableName?: string;\n}\n\nexport interface UserCreateResult {\n id: string;\n resource: { resourceType: string; id: string; [key: string]: unknown };\n meta: { lastUpdated: string; versionId: string };\n}\n\nexport async function createUserOperation(\n params: UserCreateParams,\n): Promise<UserCreateResult> {\n const { context, body, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const id = body.id ?? `user-${Date.now()}`;\n const parsedResource =\n typeof body.resource === \"string\"\n ? (JSON.parse(body.resource) as Record<string, unknown>)\n : (body.resource ?? {});\n\n const lastUpdated = context.date ?? new Date().toISOString();\n const vid = `1`;\n\n const resource = { resourceType: \"User\", id, ...parsedResource };\n const summary = JSON.stringify(extractSummary(resource as FhirResourceLike));\n\n await service.entities.user\n .put({\n id,\n resource: JSON.stringify(resource),\n summary,\n vid,\n lastUpdated,\n })\n .go();\n\n return {\n id,\n resource,\n meta: { lastUpdated, versionId: vid },\n };\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { NotFoundError } from \"../../../errors\";\nimport { OpenHiContext } from \"../../../openhi-context\";\n\nexport interface UserGetByIdParams {\n context: OpenHiContext;\n id: string;\n tableName?: string;\n}\n\nexport interface UserGetByIdResult {\n id: string;\n resource: { resourceType: string; id: string; [key: string]: unknown };\n}\n\nexport async function getUserByIdOperation(\n params: UserGetByIdParams,\n): Promise<UserGetByIdResult> {\n const { id, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const response = await service.entities.user.get({ id, sk: \"CURRENT\" }).go();\n\n const item = response.data;\n if (!item) {\n throw new NotFoundError(`User not found: ${id}`);\n }\n\n const parsedResource = JSON.parse(item.resource) as Record<string, unknown>;\n\n return {\n id,\n resource: { resourceType: \"User\", id, ...parsedResource },\n };\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { SHARD_COUNT } from \"../../../dynamo/shard\";\nimport { OpenHiContext } from \"../../../openhi-context\";\nimport {\n batchGetWithRetry,\n dispatchListMode,\n type ListOperationMode,\n} from \"../../data-operations-common\";\n\nconst SK = \"CURRENT\";\n\nexport interface UserListParams {\n context: OpenHiContext;\n tableName?: string;\n /** #853: defaults to `\"full\"`. `\"summary\"` skips BatchGet, `\"count\"` returns total only. */\n mode?: ListOperationMode;\n}\n\nexport interface UserListResult {\n entries: Array<{\n id: string;\n resource: { resourceType: string; id: string; [key: string]: unknown };\n }>;\n total: number;\n}\n\n/**\n * Lists Users via GSI1 (sharded). `mode` (default `\"full\"`) selects between BatchGet hydration,\n * summary-only (parse `summary` JSON projected on GSI1), or count-only (skip both). See\n * `dispatchListMode` in data-operations-common for the canonical mode contract.\n */\nexport async function listUsersOperation(\n params: UserListParams,\n): Promise<UserListResult> {\n const { tableName, mode = \"full\" } = params;\n const service = getDynamoControlService(tableName);\n\n const shardResults = await Promise.all(\n Array.from({ length: SHARD_COUNT }, (_, shard) =>\n service.entities.user.query.gsi1({ gsi1Shard: String(shard) }).go(),\n ),\n );\n\n return dispatchListMode<\n { id: string; resource: string },\n UserListResult[\"entries\"][number]\n >(mode, shardResults, {\n hydrate: (orderedIds) =>\n batchGetWithRetry(\n service.entities.user,\n orderedIds.map((id) => ({ id, sk: SK })),\n ) as Promise<Array<{ id: string; resource: string }>>,\n getId: (item) => item.id,\n buildEntry: (id, item) => ({\n id,\n resource: {\n resourceType: \"User\",\n id,\n ...(JSON.parse(item.resource) as Record<string, unknown>),\n },\n }),\n buildSummaryEntry: (id, parsed) => ({\n id,\n resource: { resourceType: \"User\", id, ...parsed },\n }),\n });\n}\n","import { extractSummary, type FhirResourceLike } from \"@openhi/types\";\nimport { findUserBySubOperation } from \"./user-find-by-sub-operation\";\nimport { parseUserResource } from \"./user-resource-helpers\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport {\n ForbiddenError,\n NotFoundError,\n ValidationError,\n} from \"../../../errors\";\nimport { idFromReference } from \"../../fhir-reference\";\nimport { membershipListByUserOperation } from \"../membership/membership-list-by-user-operation\";\n\nconst SK = \"CURRENT\";\n\nexport interface UserSwitchTenantWorkspaceParams {\n cognitoSub: string;\n tenantReference: string;\n workspaceReference: string;\n tableName?: string;\n /** Override the clock — used by tests for deterministic `lastUpdated`. */\n now?: () => Date;\n}\n\nexport interface UserSwitchTenantWorkspaceResult {\n id: string;\n resource: Record<string, unknown>;\n meta: { lastUpdated: string; versionId: string };\n}\n\n/**\n * Update `currentTenant` and `currentWorkspace` on the User resource for the\n * caller authenticated by the given Cognito `sub`. All other fields on the\n * User are preserved.\n *\n * Membership pre-condition uses the ADR-018 adjacency-list user projection\n * (pattern #4, workspace sub-lane). A single base-table Query on\n * `PK = USER#ID#<userId>` with\n * `SK begins_with 'MEMBERSHIP#WORKSPACE#TID#<tenantId>#'`\n * (via {@link membershipListByUserOperation} with `mode: \"workspaceInTenant\"`)\n * confirms the caller has a workspace-level Membership in the requested\n * tenant + workspace pair. No GSI1 fan-out, no scan.\n *\n * Throws:\n * - `ValidationError` when either reference is missing or malformed\n * - `NotFoundError` when no User matches the Cognito subject\n * - `ForbiddenError` when the caller has no Membership in the requested\n * `(tenantId, workspaceId)` pair on their user-partition projection\n *\n * @see https://github.com/codedrifters/openhi/issues/769\n * @see https://github.com/codedrifters/openhi/issues/1020\n * @see ADR-018 § Access Pattern Coverage (pattern #4)\n */\nexport async function switchUserTenantWorkspaceOperation(\n params: UserSwitchTenantWorkspaceParams,\n): Promise<UserSwitchTenantWorkspaceResult> {\n const { cognitoSub, tenantReference, workspaceReference, tableName } = params;\n\n const tenantId = idFromReference(tenantReference, \"Tenant/\");\n if (!tenantId) {\n throw new ValidationError(\n \"tenant.reference must be a 'Tenant/<id>' reference.\",\n );\n }\n const workspaceId = idFromReference(workspaceReference, \"Workspace/\");\n if (!workspaceId) {\n throw new ValidationError(\n \"workspace.reference must be a 'Workspace/<id>' reference.\",\n );\n }\n\n const user = await findUserBySubOperation({\n // findUserBySubOperation does not read context fields; pass a stub.\n context: {\n tenantId: \"\",\n workspaceId: \"\",\n date: \"\",\n actorId: \"\",\n actorName: \"\",\n actorType: \"internal-system\",\n },\n cognitoSub,\n tableName,\n });\n if (!user) {\n throw new NotFoundError(\n \"User not yet provisioned for the authenticated Cognito subject.\",\n );\n }\n\n // ADR-018: single Query on the user partition, narrowed to the workspace\n // sub-lane of the requested tenant. The lane includes `workspaceId` on\n // every row; a row with `workspaceId === <requested>` is sufficient and\n // necessary proof that the caller may switch to that pair.\n const projection = await membershipListByUserOperation({\n userId: user.id,\n mode: \"workspaceInTenant\",\n tenantId,\n tableName,\n });\n const hasMembership = projection.items.some(\n (row) => row.workspaceId === workspaceId,\n );\n if (!hasMembership) {\n throw new ForbiddenError(\n `User is not a member of Workspace/${workspaceId} in Tenant/${tenantId}.`,\n );\n }\n\n const existingResource = parseUserResource(user.resource) ?? {};\n const updatedResource: Record<string, unknown> = {\n ...existingResource,\n resourceType: \"User\",\n id: user.id,\n currentTenant: { reference: `Tenant/${tenantId}` },\n currentWorkspace: { reference: `Workspace/${workspaceId}` },\n };\n\n const lastUpdated = (params.now ? params.now() : new Date()).toISOString();\n const vid = `${Date.now()}`;\n const summary = JSON.stringify(\n extractSummary(updatedResource as FhirResourceLike),\n );\n\n const service = getDynamoControlService(tableName);\n await service.entities.user\n .patch({ id: user.id, sk: SK })\n .set({\n resource: JSON.stringify(updatedResource),\n summary,\n vid,\n lastUpdated,\n })\n .go();\n\n return {\n id: user.id,\n resource: updatedResource,\n meta: { lastUpdated, versionId: vid },\n };\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\n\n/**\n * Filter modes for {@link membershipListByUserOperation}.\n *\n * Maps directly to the ADR-018 sub-lane discriminator in the user-projection\n * SK (`MEMBERSHIP#TENANT#…` vs `MEMBERSHIP#WORKSPACE#…`):\n *\n * - `\"all\"` — `Query(PK = USER#ID#<userId>, SK begins_with 'MEMBERSHIP#')`.\n * Returns both lanes interleaved in raw SK order.\n * - `\"tenant\"` — `SK begins_with 'MEMBERSHIP#TENANT#'`. Pattern #3 only.\n * - `\"workspace\"` — `SK begins_with 'MEMBERSHIP#WORKSPACE#'`. Pattern #4\n * across every tenant.\n * - `\"workspaceInTenant\"` — `SK begins_with 'MEMBERSHIP#WORKSPACE#TID#<tenantId>#'`.\n * Pattern #4 narrowed to one tenant. Requires `tenantId`.\n */\nexport type MembershipListByUserMode =\n | \"all\"\n | \"tenant\"\n | \"workspace\"\n | \"workspaceInTenant\";\n\n/** Inputs accepted by {@link membershipListByUserOperation}. */\nexport interface MembershipListByUserParams {\n readonly userId: string;\n /** Filter mode — see {@link MembershipListByUserMode}. Defaults to `\"all\"`. */\n readonly mode?: MembershipListByUserMode;\n /** Required only when `mode === \"workspaceInTenant\"`. */\n readonly tenantId?: string;\n /** ElectroDB cursor from a prior page. Forwarded to `.go({ cursor })`. */\n readonly cursor?: string | null;\n /** Per-page item limit forwarded to `.go({ limit })`. */\n readonly limit?: number;\n /** Sort order forwarded to `.go({ order })`. Defaults to ElectroDB's `\"asc\"`. */\n readonly order?: \"asc\" | \"desc\";\n /** Optional table-name override; resolved via env when omitted. */\n readonly tableName?: string;\n}\n\n/** One projection-row payload as returned to a consumer. */\nexport interface MembershipUserProjectionEntry {\n readonly userId: string;\n readonly sk: string;\n readonly tenantId: string;\n readonly workspaceId?: string;\n readonly membershipId: string;\n readonly summary: string;\n readonly vid: string;\n readonly lastUpdated: string;\n readonly denormalizedTenantName?: string;\n readonly denormalizedUserName?: string;\n readonly denormalizedWorkspaceName?: string;\n}\n\n/** Page returned by {@link membershipListByUserOperation}. */\nexport interface MembershipListByUserResult {\n readonly items: Array<MembershipUserProjectionEntry>;\n /** ElectroDB cursor for the next page, or `null` when exhausted. */\n readonly cursor: string | null;\n}\n\n/**\n * Compose the SK prefix for a given filter mode. Centralizing the\n * prefix string here keeps the SK grammar (owned by\n * `membership-user-projection.ts`) the single source of truth for the\n * lane discriminators — this function reads them, it does not invent them.\n */\nfunction buildSkPrefix(\n mode: MembershipListByUserMode,\n tenantId: string | undefined,\n): string {\n switch (mode) {\n case \"tenant\":\n return \"MEMBERSHIP#TENANT#\";\n case \"workspace\":\n return \"MEMBERSHIP#WORKSPACE#\";\n case \"workspaceInTenant\":\n // Pattern-#4 SK places `<tenantId>` directly after the\n // `MEMBERSHIP#WORKSPACE#TID#` segment so a `begins_with` filter\n // narrows the workspace lane to a single tenant.\n return `MEMBERSHIP#WORKSPACE#TID#${tenantId}#`;\n case \"all\":\n default:\n return \"MEMBERSHIP#\";\n }\n}\n\n/**\n * List Memberships for a user via the ADR-018 user-partition projection\n * (no GSI hop).\n *\n * Reads `MembershipUserProjectionEntity` rows under `PK = USER#ID#<userId>`\n * with an `SK begins_with` filter selected by `mode`:\n *\n * | Mode | SK begins_with | Covers |\n * |---|---|---|\n * | `all` (default) | `MEMBERSHIP#` | patterns #3 + #4 interleaved |\n * | `tenant` | `MEMBERSHIP#TENANT#` | pattern #3 only |\n * | `workspace` | `MEMBERSHIP#WORKSPACE#` | pattern #4 only, across tenants |\n * | `workspaceInTenant` | `MEMBERSHIP#WORKSPACE#TID#<tenantId>#` | pattern #4 in one tenant |\n *\n * Returns the projection rows verbatim (`summary`, `vid`, `lastUpdated`\n * plus the projection-discriminating fields) — full canonical-resource\n * hydration is opt-in for callers via\n * `MembershipEntity.get({ tenantId, id: membershipId })`. Pagination\n * mirrors ElectroDB's native `.go({ cursor })` shape; the returned\n * `cursor` is opaque to callers.\n *\n * @see ADR-018 § Access Pattern Coverage (patterns #3 and #4)\n * @see .state/adr-018-implementation-guide.md § 1 (SK grammar)\n */\nexport async function membershipListByUserOperation(\n params: MembershipListByUserParams,\n): Promise<MembershipListByUserResult> {\n const {\n userId,\n mode = \"all\",\n tenantId,\n cursor = null,\n limit,\n order,\n tableName,\n } = params;\n\n if (mode === \"workspaceInTenant\" && !tenantId) {\n throw new Error(\n 'membershipListByUserOperation: tenantId is required when mode === \"workspaceInTenant\"',\n );\n }\n\n const service = getDynamoControlService(tableName);\n const skPrefix = buildSkPrefix(mode, tenantId);\n\n const goOptions: {\n cursor?: string | null;\n limit?: number;\n order?: \"asc\" | \"desc\";\n } = {\n cursor,\n };\n if (limit !== undefined) {\n goOptions.limit = limit;\n }\n if (order !== undefined) {\n goOptions.order = order;\n }\n\n const result = await service.entities.membershipUserProjection.query\n .record({ userId })\n .begins({ sk: skPrefix })\n .go(goOptions);\n\n const items: Array<MembershipUserProjectionEntry> = (result.data ?? []).map(\n (row) => ({\n userId: row.userId,\n sk: row.sk,\n tenantId: row.tenantId,\n workspaceId: row.workspaceId,\n membershipId: row.membershipId,\n summary: row.summary,\n vid: row.vid,\n lastUpdated: row.lastUpdated,\n denormalizedTenantName: row.denormalizedTenantName,\n denormalizedUserName: row.denormalizedUserName,\n denormalizedWorkspaceName: row.denormalizedWorkspaceName,\n }),\n );\n\n return { items, cursor: result.cursor ?? null };\n}\n","import { extractSummary, type FhirResourceLike } from \"@openhi/types\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { NotFoundError } from \"../../../errors\";\nimport { OpenHiContext } from \"../../../openhi-context\";\n\nexport interface UserUpdateParams {\n context: OpenHiContext;\n id: string;\n body: { resource?: Record<string, unknown> | string };\n tableName?: string;\n}\n\nexport interface UserUpdateResult {\n id: string;\n resource: { resourceType: string; id: string; [key: string]: unknown };\n meta: { lastUpdated: string; versionId: string };\n}\n\nexport async function updateUserOperation(\n params: UserUpdateParams,\n): Promise<UserUpdateResult> {\n const { context, id, body, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const existing = await service.entities.user.get({ id, sk: \"CURRENT\" }).go();\n if (!existing.data) {\n throw new NotFoundError(`User not found: ${id}`);\n }\n\n const parsedResource =\n typeof body.resource === \"string\"\n ? (JSON.parse(body.resource) as Record<string, unknown>)\n : (body.resource ?? {});\n\n const lastUpdated = context.date ?? new Date().toISOString();\n const vid = `${Date.now()}`;\n\n const resource = { resourceType: \"User\", id, ...parsedResource };\n const summary = JSON.stringify(extractSummary(resource as FhirResourceLike));\n\n await service.entities.user\n .put({\n id,\n resource: JSON.stringify(resource),\n summary,\n vid,\n lastUpdated,\n })\n .go();\n\n return {\n id,\n resource,\n meta: { lastUpdated, versionId: vid },\n };\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { OpenHiContext } from \"../../../openhi-context\";\n\nexport interface UserDeleteParams {\n context: OpenHiContext;\n id: string;\n tableName?: string;\n}\n\nexport async function deleteUserOperation(\n params: UserDeleteParams,\n): Promise<void> {\n const { id, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n await service.entities.user.delete({ id, sk: \"CURRENT\" }).go();\n}\n"],"mappings":"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","names":["extractSummary","SK","extractSummary","extractSummary","extractSummary"]}
|
|
@@ -329,11 +329,11 @@ var ConfigurationUserProjectionEntity = new Entity2({
|
|
|
329
329
|
},
|
|
330
330
|
indexes: {
|
|
331
331
|
/**
|
|
332
|
-
* Base table: PK = USER#ID
|
|
333
|
-
* single `Query(PK = USER#ID#<userId>, SK begins_with
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
332
|
+
* Base table: PK = USER#ID#\<userId\>, SK = operation-supplied. A
|
|
333
|
+
* single `Query(PK = USER#ID#<userId>, SK begins_with 'CONFIGURATION#')`
|
|
334
|
+
* returns the user's user-scoped Configurations sorted by
|
|
335
|
+
* `<normalizedConfigName>` (then `<configurationId>` as the
|
|
336
|
+
* tiebreaker).
|
|
337
337
|
*/
|
|
338
338
|
record: {
|
|
339
339
|
pk: {
|
|
@@ -721,11 +721,12 @@ var MembershipUserProjectionEntity = new Entity5({
|
|
|
721
721
|
},
|
|
722
722
|
indexes: {
|
|
723
723
|
/**
|
|
724
|
-
* Base table: PK = USER#ID
|
|
724
|
+
* Base table: PK = USER#ID#\<userId\>, SK = operation-supplied.
|
|
725
725
|
* Both pattern #3 and pattern #4 use this same index — the SK string
|
|
726
726
|
* encodes the lane discriminator (`MEMBERSHIP#TENANT#…` vs
|
|
727
|
-
* `MEMBERSHIP#WORKSPACE#…`) so a single
|
|
728
|
-
* SK begins_with 'MEMBERSHIP#')`
|
|
727
|
+
* `MEMBERSHIP#WORKSPACE#…`) so a single
|
|
728
|
+
* `Query(PK = USER#ID#<userId>, SK begins_with 'MEMBERSHIP#')`
|
|
729
|
+
* returns both lanes interleaved.
|
|
729
730
|
*/
|
|
730
731
|
record: {
|
|
731
732
|
pk: {
|
|
@@ -1228,12 +1229,12 @@ var RoleAssignmentUserProjectionEntity = new Entity9({
|
|
|
1228
1229
|
},
|
|
1229
1230
|
indexes: {
|
|
1230
1231
|
/**
|
|
1231
|
-
* Base table: PK = USER#ID
|
|
1232
|
+
* Base table: PK = USER#ID#\<userId\>, SK = operation-supplied. Both
|
|
1232
1233
|
* sub-lanes (tenant-level and workspace-level) use this same index —
|
|
1233
1234
|
* the SK string encodes the lane discriminator
|
|
1234
1235
|
* (`ROLEASSIGNMENT#TENANT#…` vs `ROLEASSIGNMENT#WORKSPACE#…`) so a
|
|
1235
|
-
* single `Query(PK = USER#ID#<userId>, SK begins_with
|
|
1236
|
-
*
|
|
1236
|
+
* single `Query(PK = USER#ID#<userId>, SK begins_with 'ROLEASSIGNMENT#')`
|
|
1237
|
+
* returns both lanes interleaved.
|
|
1237
1238
|
*/
|
|
1238
1239
|
record: {
|
|
1239
1240
|
pk: {
|
|
@@ -1800,4 +1801,4 @@ export {
|
|
|
1800
1801
|
computeShard,
|
|
1801
1802
|
getDynamoControlService
|
|
1802
1803
|
};
|
|
1803
|
-
//# sourceMappingURL=chunk-
|
|
1804
|
+
//# sourceMappingURL=chunk-VZCPGQXA.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/data/dynamo/dynamo-control-service.ts","../src/data/dynamo/entities/control/configuration-entity.ts","../src/data/dynamo/entities/control/control-entity-common.ts","../src/data/dynamo/shard.ts","../src/data/dynamo/entities/control/configuration-user-projection-entity.ts","../src/data/dynamo/entities/control/configuration-workspace-projection-entity.ts","../src/data/dynamo/entities/control/membership-entity.ts","../src/data/dynamo/entities/control/membership-user-projection-entity.ts","../src/data/dynamo/entities/control/membership-workspace-projection-entity.ts","../src/data/dynamo/entities/control/role-entity.ts","../src/data/dynamo/entities/control/roleassignment-entity.ts","../src/data/dynamo/entities/control/roleassignment-user-projection-entity.ts","../src/data/dynamo/entities/control/roleassignment-workspace-projection-entity.ts","../src/data/dynamo/entities/control/tenant-entity.ts","../src/data/dynamo/entities/control/user-entity.ts","../src/data/dynamo/entities/control/workspace-entity.ts"],"sourcesContent":["import { Service } from \"electrodb\";\nimport { defaultTableName, dynamoClient } from \"./dynamo-client\";\nimport { ConfigurationEntity } from \"./entities/control/configuration-entity\";\nimport { ConfigurationUserProjectionEntity } from \"./entities/control/configuration-user-projection-entity\";\nimport { ConfigurationWorkspaceProjectionEntity } from \"./entities/control/configuration-workspace-projection-entity\";\nimport { MembershipEntity } from \"./entities/control/membership-entity\";\nimport { MembershipUserProjectionEntity } from \"./entities/control/membership-user-projection-entity\";\nimport { MembershipWorkspaceProjectionEntity } from \"./entities/control/membership-workspace-projection-entity\";\nimport { RoleEntity } from \"./entities/control/role-entity\";\nimport { RoleAssignmentEntity } from \"./entities/control/roleassignment-entity\";\nimport { RoleAssignmentUserProjectionEntity } from \"./entities/control/roleassignment-user-projection-entity\";\nimport { RoleAssignmentWorkspaceProjectionEntity } from \"./entities/control/roleassignment-workspace-projection-entity\";\nimport { TenantEntity } from \"./entities/control/tenant-entity\";\nimport { UserEntity } from \"./entities/control/user-entity\";\nimport { WorkspaceEntity } from \"./entities/control/workspace-entity\";\n\n/**\n * Control-plane entities only (service \"control\"). Same table as data plane; use\n * DynamoDataService for data-plane entities.\n * @see sites/www-docs/content/packages/@openhi/constructs/data/dynamo/single-table-design.md\n * @see sites/www-docs/content/architecture/adr/2026-03-03-01-tenant-isolated-vs-non-tenant-isolated-entities.md\n */\n\nconst controlPlaneEntities = {\n configuration: ConfigurationEntity,\n configurationUserProjection: ConfigurationUserProjectionEntity,\n configurationWorkspaceProjection: ConfigurationWorkspaceProjectionEntity,\n membership: MembershipEntity,\n membershipUserProjection: MembershipUserProjectionEntity,\n membershipWorkspaceProjection: MembershipWorkspaceProjectionEntity,\n role: RoleEntity,\n roleAssignment: RoleAssignmentEntity,\n roleAssignmentUserProjection: RoleAssignmentUserProjectionEntity,\n roleAssignmentWorkspaceProjection: RoleAssignmentWorkspaceProjectionEntity,\n tenant: TenantEntity,\n user: UserEntity,\n workspace: WorkspaceEntity,\n};\n\nconst controlPlaneService = new Service(controlPlaneEntities, {\n table: defaultTableName,\n client: dynamoClient,\n});\n\n/**\n * Control-plane service: entities for configuration and control. Use with the\n * data store table (PK, SK, GSI1; UserEntity also uses GSI2).\n *\n * `transaction` exposes ElectroDB's `service.transaction.write` /\n * `service.transaction.get` builders so the operations-layer multi-write\n * helper (#1010, ADR-018) can compose `TransactWriteItems` across the\n * control-plane entities.\n */\nexport const DynamoControlService = {\n entities: controlPlaneService.entities,\n transaction: controlPlaneService.transaction,\n};\n\nexport type DynamoControlServiceType = typeof DynamoControlService;\n\n/**\n * Returns the control-plane service. Table name is resolved from tableName (optional override),\n * then DYNAMO_TABLE_NAME, then \"jesttesttable\".\n */\nexport function getDynamoControlService(\n tableName?: string,\n): DynamoControlServiceType {\n const resolved = tableName ?? defaultTableName;\n const service = new Service(controlPlaneEntities, {\n table: resolved,\n client: dynamoClient,\n });\n return {\n entities: service.entities,\n transaction: service.transaction,\n };\n}\n","import { Entity } from \"electrodb\";\nimport { gsi1ShardAttribute } from \"./control-entity-common\";\n\n/**\n * Configuration data-store entity (single-table store).\n *\n * **Classification (ADR 2026-03-03-01):** Partially tenant-isolated, control plane. Cascade of scope\n * levels: resolution order user → workspace → tenant → baseline. Sentinels: tenantId \"BASELINE\" for\n * baseline tier; workspaceId/userId/roleId \"-\" for absent dimension.\n *\n * Key structure: PK = CONFIG#TID#<tenantId>#WID#<workspaceId>#UID#<userId>#RID#<roleId>,\n * SK = KEY#<key>#SK#<sk>. Uniqueness: one Configuration per (tenantId, workspaceId, userId, roleId, key).\n * Standard attributes and key-building conventions align with single-table design.\n *\n * GSI1 — Unified Sharded List per ADR-011: lists all Configuration entries in a tenant/workspace\n * across the four shards.\n *\n * @see sites/www-docs/content/architecture/adr/2026-03-03-01-tenant-isolated-vs-non-tenant-isolated-entities.md\n * @see sites/www-docs/content/packages/@openhi/constructs/data/dynamo/entities/configuration.md\n * @see sites/www-docs/content/architecture/control-plane/configuration.md\n * @see sites/www-docs/content/packages/@openhi/constructs/data/dynamo/single-table-design.md\n * @see sites/www-docs/content/packages/@openhi/constructs/data/dynamo/entity-standards.md — Key-building conventions (keys built inside entity)\n */\nexport const ConfigurationEntity = new Entity({\n model: {\n entity: \"configuration\",\n service: \"control\",\n version: \"01\",\n },\n attributes: {\n /** Sort key. \"CURRENT\" for current version; version history in S3. */\n sk: {\n type: \"string\" as const,\n required: true,\n default: \"CURRENT\",\n },\n /** Tenant scope. Use \"BASELINE\" when the config is baseline default (no tenant). */\n tenantId: {\n type: \"string\" as const,\n required: true,\n default: \"BASELINE\",\n },\n /** Workspace scope. Use \"-\" when absent. */\n workspaceId: {\n type: \"string\" as const,\n required: true,\n default: \"-\",\n },\n /** User scope. Use \"-\" when absent. */\n userId: {\n type: \"string\" as const,\n required: true,\n default: \"-\",\n },\n /** Role scope. Use \"-\" when absent. */\n roleId: {\n type: \"string\" as const,\n required: true,\n default: \"-\",\n },\n /** Config type (category), e.g. endpoints, branding, display. */\n key: {\n type: \"string\" as const,\n required: true,\n },\n /** FHIR Resource.id; logical id in URL and for the Configuration resource. */\n id: {\n type: \"string\" as const,\n required: true,\n },\n /** Payload as JSON string. JSON.stringify(resource) on write; JSON.parse(item.resource) on read. */\n resource: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Summary projection (key display fields as JSON string: id, key, status).\n * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.\n */\n summary: {\n type: \"string\" as const,\n required: true,\n },\n /** Version id (e.g. ULID). Tracks current version; S3 history key. */\n vid: {\n type: \"string\" as const,\n required: true,\n },\n lastUpdated: {\n type: \"string\" as const,\n required: true,\n },\n gsi1Shard: gsi1ShardAttribute,\n deleted: {\n type: \"boolean\" as const,\n required: false,\n },\n bundleId: {\n type: \"string\" as const,\n required: false,\n },\n msgId: {\n type: \"string\" as const,\n required: false,\n },\n },\n indexes: {\n /** Base table: PK, SK (data store key names). PK is built from tenantId, workspaceId, userId, roleId; SK is built from key and sk. Do not supply PK or SK from outside. */\n record: {\n pk: {\n field: \"PK\",\n composite: [\"tenantId\", \"workspaceId\", \"userId\", \"roleId\"],\n template:\n \"CONFIG#TID#${tenantId}#WID#${workspaceId}#UID#${userId}#RID#${roleId}\",\n },\n sk: {\n field: \"SK\",\n composite: [\"key\", \"sk\"],\n template: \"KEY#${key}#SK#${sk}\",\n },\n },\n\n /**\n * GSI1 — Unified Sharded List per ADR-011: list all Configuration entries for a\n * (tenant, workspace) across the four shards. Use for \"list configs scoped to this tenant\"\n * (workspaceId = \"-\") or \"list configs scoped to this workspace\". Does not support\n * hierarchical resolution in one query; use base table GetItem in fallback order\n * (user → workspace → tenant → baseline) for that.\n * SK is `<key>#<id>` — Configuration's `key` is a required entity attribute (the\n * config category: endpoints, branding, display, …) and the natural sort/lookup\n * dimension. `casing: \"none\"` preserves the literal key value.\n */\n gsi1: {\n index: \"GSI1\",\n pk: {\n field: \"GSI1PK\",\n composite: [\"tenantId\", \"workspaceId\", \"gsi1Shard\"],\n template:\n \"TID#${tenantId}#WID#${workspaceId}#RT#Configuration#SHARD#${gsi1Shard}\",\n },\n sk: {\n field: \"GSI1SK\",\n casing: \"none\" as const,\n composite: [\"key\", \"id\"],\n template: \"${key}#${id}\",\n },\n },\n },\n});\n","import { extractLabel, normalizeLabel } from \"@openhi/types\";\nimport { computeShard } from \"../../shard\";\n\n/**\n * Shared GSI1 shard attribute for control-plane entities.\n *\n * Control-plane entities (User, Tenant, Workspace, Membership, Role, RoleAssignment,\n * Configuration) use the same `TID#/WID#/RT#/SHARD#` PK shape on GSI1 as data-plane\n * FHIR resources per ADR-011. The shard index is derived deterministically from `id`\n * via `computeShard` so updates always land on the same shard. Stored as a string\n * because it appears as a literal segment in the GSI1 PK template; the underlying\n * value is 0..3.\n *\n * Not `required` because the value is derived via `watch`/`set`; ElectroDB's\n * required-field check runs before watch propagation, so callers must not fail\n * validation on a derived field.\n */\nexport const gsi1ShardAttribute = {\n type: \"string\" as const,\n watch: [\"id\"] as const,\n set: (_val?: string, item?: { id?: string }) => {\n if (typeof item?.id !== \"string\" || item.id.length === 0) {\n return undefined;\n }\n return String(computeShard(item.id));\n },\n};\n\n/**\n * Shared GSI1 sort-key attribute for control-plane entities.\n *\n * Derives the GSI1SK value at write time from the entity's `resource` JSON\n * string, applying the same label-extraction strategy as the data plane\n * (DR-004 / `@openhi/types` `extractLabel`). When the resource carries a\n * natural label (Tenant.name, Workspace.name, Configuration.key, …) the\n * sort key is `<normalizedLabel>#<id>` so list endpoints sort alphabetically\n * and `BEGINS_WITH` queries serve prefix searches. When no label is\n * extractable, falls back to `<entity.lastUpdated>#<id>` for stable\n * reverse-chronological ordering.\n *\n * Falls back gracefully on malformed `resource` payloads — JSON parse\n * failures and missing fields both route to the lastUpdated fallback so a\n * single bad write never blocks an entity put. The entity-level\n * `lastUpdated` is preferred over `resource.meta.lastUpdated` so the\n * fallback uses the authoritative timestamp the entity write supplies.\n *\n * Not `required` because the value is derived via `watch`/`set`.\n */\nexport const gsi1skAttribute = {\n type: \"string\" as const,\n watch: [\"resource\", \"lastUpdated\", \"id\"] as const,\n set: (\n _val?: string,\n item?: { resource?: string; lastUpdated?: string; id?: string },\n ) => {\n const id = typeof item?.id === \"string\" ? item.id : \"\";\n const lastUpdated =\n typeof item?.lastUpdated === \"string\" ? item.lastUpdated : \"\";\n const fallback = `${lastUpdated}#${id}`;\n\n if (typeof item?.resource !== \"string\" || item.resource.length === 0) {\n return fallback;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(item.resource);\n } catch {\n return fallback;\n }\n if (!parsed || typeof parsed !== \"object\") return fallback;\n const resourceType = (parsed as { resourceType?: unknown }).resourceType;\n if (typeof resourceType !== \"string\") return fallback;\n\n const label = extractLabel(parsed as Parameters<typeof extractLabel>[0]);\n return label !== undefined ? `${label}#${id}` : fallback;\n },\n};\n\n/**\n * Extract a roleId from a RoleAssignment resource payload. Looks first at\n * a flat top-level `roleId` string, then at a FHIR-style `role.reference`\n * (e.g. `Role/<id>`). Returns `undefined` when neither shape is present\n * or the field is malformed — callers fall back to the generic GSI1SK\n * derivation in that case so a single bad write never blocks an entity put.\n */\nfunction extractRoleId(resource: Record<string, unknown>): string | undefined {\n const flat = resource.roleId;\n if (typeof flat === \"string\" && flat.length > 0) return flat;\n\n const role = resource.role;\n if (role && typeof role === \"object\") {\n const reference = (role as { reference?: unknown }).reference;\n if (typeof reference === \"string\" && reference.length > 0) {\n const slash = reference.lastIndexOf(\"/\");\n const tail = slash >= 0 ? reference.slice(slash + 1) : reference;\n if (tail.length > 0) return tail;\n }\n }\n return undefined;\n}\n\n/**\n * RoleAssignment-specific GSI1 sort-key attribute (ADR-018 pattern #8 —\n * \"users with a specific role in a tenant, sorted by user name\").\n *\n * Composes the canonical-row GSI1SK as the discriminator-first shape\n * `<roleId>#<normalizedUserName>#<id>` so a GSI1 query partitioned on\n * the tenant can `begins_with('<roleId>#')` to enumerate every user\n * assigned to a given role, sorted by user name.\n *\n * - `<roleId>` is read from a flat `resource.roleId` field, falling back\n * to the slug after the final `/` in `resource.role.reference` (the\n * FHIR Reference shape). Sorting on `roleId` rather than the role's\n * display name means a Role rename does not cascade onto this index\n * (TR-024 / ADR-018 § Implementation Notes).\n * - `<normalizedUserName>` is `normalizeLabel(denormalizedUserName)` —\n * the top-level denormalized field promoted in #1009 (TR-024 rule 3:\n * canonical-record symmetry).\n *\n * Falls back to `gsi1skAttribute`'s `<lastUpdated>#<id>` shape when\n * either component is missing or malformed, so pre-TR-024 rows and\n * malformed payloads still produce a valid sort key.\n *\n * Not `required` because the value is derived via `watch`/`set`.\n *\n * @see ADR-018 § Access Pattern Coverage — pattern #8\n * @see TR-024 — Denormalized display-name attributes\n */\nexport const roleAssignmentGsi1skAttribute = {\n type: \"string\" as const,\n watch: [\"resource\", \"denormalizedUserName\", \"lastUpdated\", \"id\"] as const,\n set: (\n _val?: string,\n item?: {\n resource?: string;\n denormalizedUserName?: string;\n lastUpdated?: string;\n id?: string;\n },\n ) => {\n const id = typeof item?.id === \"string\" ? item.id : \"\";\n const lastUpdated =\n typeof item?.lastUpdated === \"string\" ? item.lastUpdated : \"\";\n const fallback = `${lastUpdated}#${id}`;\n\n if (typeof item?.resource !== \"string\" || item.resource.length === 0) {\n return fallback;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(item.resource);\n } catch {\n return fallback;\n }\n if (!parsed || typeof parsed !== \"object\") return fallback;\n\n const roleId = extractRoleId(parsed as Record<string, unknown>);\n if (roleId === undefined) return fallback;\n\n const denormalizedUserName =\n typeof item.denormalizedUserName === \"string\"\n ? item.denormalizedUserName\n : \"\";\n const normalizedUserName =\n denormalizedUserName.length > 0\n ? normalizeLabel(denormalizedUserName)\n : \"\";\n if (normalizedUserName.length === 0) return fallback;\n\n return `${roleId}#${normalizedUserName}#${id}`;\n },\n};\n\n/**\n * Membership-specific GSI1 sort-key attribute (ADR-018 pattern #1 —\n * \"users in a tenant, sorted by user name\").\n *\n * Composes the canonical-row GSI1SK as `<normalizedUserName>#<id>` so a\n * GSI1 query partitioned on the tenant range-scans by user-name prefix\n * and returns memberships sorted alphabetically by user name. No role\n * discriminator goes in front — pattern #1 is user-name-first.\n *\n * - `<normalizedUserName>` is `normalizeLabel(denormalizedUserName)` —\n * the top-level denormalized field promoted in #1009 (TR-024 rule 3:\n * canonical-record symmetry).\n *\n * Falls back to `gsi1skAttribute`'s `<lastUpdated>#<id>` shape when\n * `denormalizedUserName` is missing, so pre-TR-024 rows and malformed\n * payloads still produce a valid sort key.\n *\n * Not `required` because the value is derived via `watch`/`set`.\n *\n * @see ADR-018 § Access Pattern Coverage — pattern #1\n * @see TR-024 — Denormalized display-name attributes\n */\nexport const membershipGsi1skAttribute = {\n type: \"string\" as const,\n watch: [\"denormalizedUserName\", \"lastUpdated\", \"id\"] as const,\n set: (\n _val?: string,\n item?: {\n denormalizedUserName?: string;\n lastUpdated?: string;\n id?: string;\n },\n ) => {\n const id = typeof item?.id === \"string\" ? item.id : \"\";\n const lastUpdated =\n typeof item?.lastUpdated === \"string\" ? item.lastUpdated : \"\";\n const fallback = `${lastUpdated}#${id}`;\n\n const denormalizedUserName =\n typeof item?.denormalizedUserName === \"string\"\n ? item.denormalizedUserName\n : \"\";\n const normalizedUserName =\n denormalizedUserName.length > 0\n ? normalizeLabel(denormalizedUserName)\n : \"\";\n if (normalizedUserName.length === 0) {\n return fallback;\n }\n\n return `${normalizedUserName}#${id}`;\n },\n};\n","/**\n * Shard selection for the data-plane single-table GSI1 partitioning per ADR-011.\n *\n * GSI1's partition key embeds a `SHARD#<n>` segment with `n = computeShard(id)`.\n * The hash is deterministic so updates to the same resource id always land on\n * the same shard (no cross-shard migration on update); reads fan out to all\n * shards in parallel and merge by SK.\n *\n * @see sites/www-docs/content/architecture/adr/ — ADR-011 (single-table DynamoDB)\n */\n\n/** Number of shards in the GSI1 partition key. Fixed at 4 in v1; raising it later is a backfill, not a schema migration. */\nexport const SHARD_COUNT = 4;\n\n/**\n * Returns a deterministic shard index in [0, SHARD_COUNT) for the given resource id.\n *\n * Implementation: 32-bit FNV-1a over the UTF-16 code units of the id, modulo SHARD_COUNT.\n * The function is pure and stable; the same id always returns the same shard.\n *\n * ESLint's `no-bitwise` rule is disabled inside this function because FNV-1a is\n * defined in terms of XOR and unsigned-right-shift — the bitwise ops are the\n * algorithm, not an accidental logical-operator confusion.\n */\nexport function computeShard(id: string): number {\n /* eslint-disable no-bitwise */\n let hash = 0x811c9dc5;\n for (let i = 0; i < id.length; i++) {\n hash ^= id.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193);\n }\n return (hash >>> 0) % SHARD_COUNT;\n /* eslint-enable no-bitwise */\n}\n","import { Entity } from \"electrodb\";\n\n/**\n * Configuration user-projection entity (single-table store, no GSI).\n *\n * **ADR-018 adjacency-list projection — pattern #10 (user-scope half).**\n * For every user-scoped Configuration write the operations-layer\n * multi-write helper writes one projection row under the user partition\n * so the user-rooted access pattern #10 is served by a single\n * base-table `Query` with no GSI hop:\n *\n * | Pattern | When | PK | SK |\n * |---|---|---|---|\n * | #10 user-scope | Configuration is user-scoped (`userId !== \"-\"`) | `USER#ID#<userId>` | `CONFIGURATION#<normalizedConfigName>#<configurationId>` |\n *\n * `<normalizedConfigName>` derives from Configuration's `key` attribute\n * (the canonical name dimension — Configuration carries no `displayName`\n * per TR-024 § Open Item #5, so `key` is the natural sort source). The\n * SK shape is operation-owned: the operations-layer projection writer\n * composes the SK string via `buildConfigurationUserProjectionSk` and\n * supplies it on the `sk` attribute. This entity stores the SK verbatim —\n * no `watch`/derived computation here — so the SK grammar (and any\n * future revision) lives in one place: the operations layer.\n *\n * Projection attribute set per ADR-018 § Projection attribute set and\n * the implementation guide § 2: `summary`, `vid`, `lastUpdated` (so\n * `Query(PK = USER#ID#<userId>, SK begins_with 'CONFIGURATION#')` is\n * self-sufficient — no BatchGet hop to the canonical record), plus the\n * projection-discriminating fields (`configurationId`, `userId`,\n * `tenantId`, `scope`).\n *\n * **Cross-tenant partition.** Unlike Membership/RoleAssignment-workspace\n * partitions, the Configuration user-projection's PK carries no tenant\n * prefix — a user's user-scoped Configurations are cross-tenant by\n * design (a user may carry preferences that follow them across tenant\n * memberships). This mirrors the RoleAssignment user-projection partition.\n *\n * **No GSI projection.** Per ADR-018 § Decision, cross-cutting reads\n * are served by the main-table partition `USER#ID#<userId>`; the\n * GSI1/GSI2 catalog is unchanged. Tenant-scoped Configurations\n * continue to use the canonical GSI1 path (ADR-011) unchanged.\n *\n * @see ADR-018 § Access Pattern Coverage (#10 — user-scope half)\n * @see .state/adr-018-implementation-guide.md § 1 (SK grammar) and § 2 (attribute set)\n * @see .claude/rules/data-layer-layout.md — projection writers live in operations, not here\n */\nexport const ConfigurationUserProjectionEntity = new Entity({\n model: {\n entity: \"configurationUserProjection\",\n service: \"control\",\n version: \"01\",\n },\n attributes: {\n /**\n * User partition discriminator. Renders as `USER#ID#<userId>` on the\n * base-table PK. Always required — the projection has no meaning\n * outside a user partition.\n */\n userId: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Pre-composed sort key — built by the operations-layer projection\n * writer via `buildConfigurationUserProjectionSk`. The entity stores\n * the value verbatim so the SK grammar (pattern #10 user-scope) is\n * owned by the operations layer, not duplicated here.\n */\n sk: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Configuration canonical-record id. Stored as a discriminating\n * field so consumers can hydrate the canonical row via the\n * Configuration get-by-id operation when the projection's `summary`\n * is insufficient.\n */\n configurationId: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Tenant the Configuration is associated with. The canonical row\n * keys off `(tenantId, workspaceId, userId, roleId)`; the projection\n * carries `tenantId` so consumers reconstructing the canonical PK\n * have the tenant segment without a hop.\n */\n tenantId: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Scope marker. Always `\"user\"` on this projection — recorded\n * explicitly so future scope-bearing projections (workspace,\n * tenant, role) can share filter semantics in a unified\n * cross-projection list query if one ever lands.\n */\n scope: {\n type: \"string\" as const,\n required: true,\n default: \"user\",\n },\n /**\n * Configuration's `key` attribute (config category, e.g. endpoints,\n * branding, display). Mirrored from the canonical row so consumers\n * reading the projection get the natural display label without a\n * BatchGet hop. Doubles as the source of `<normalizedConfigName>` in\n * the SK.\n */\n displayName: {\n type: \"string\" as const,\n required: false,\n },\n /**\n * Summary projection (key display fields as JSON string) — mirrored\n * from the canonical Configuration row so user-partition queries do\n * not need a BatchGet hop.\n */\n summary: {\n type: \"string\" as const,\n required: true,\n },\n /** Version id mirrored from the canonical Configuration row. */\n vid: {\n type: \"string\" as const,\n required: true,\n },\n /** Last-updated timestamp mirrored from the canonical Configuration row. */\n lastUpdated: {\n type: \"string\" as const,\n required: true,\n },\n },\n indexes: {\n /**\n * Base table: PK = USER#ID#\\<userId\\>, SK = operation-supplied. A\n * single `Query(PK = USER#ID#<userId>, SK begins_with 'CONFIGURATION#')`\n * returns the user's user-scoped Configurations sorted by\n * `<normalizedConfigName>` (then `<configurationId>` as the\n * tiebreaker).\n */\n record: {\n pk: {\n field: \"PK\",\n composite: [\"userId\"],\n template: \"USER#ID#${userId}\",\n },\n sk: {\n field: \"SK\",\n casing: \"none\" as const,\n composite: [\"sk\"],\n template: \"${sk}\",\n },\n },\n },\n});\n","import { Entity } from \"electrodb\";\n\n/**\n * Configuration workspace-projection entity (single-table store, no GSI).\n *\n * **ADR-018 adjacency-list projection — pattern #10 (workspace-scope half).**\n * For every workspace-scoped Configuration the operations-layer\n * multi-write helper writes one projection row under the workspace\n * partition so the workspace-rooted access pattern #10 is served by a\n * single base-table `Query` with no GSI hop:\n *\n * | Pattern | When | PK | SK |\n * |---|---|---|---|\n * | #10 workspace-scope | Configuration is workspace-scoped (`workspaceId !== \"-\"`, `userId === \"-\"`) | `TID#<tenantId>#WORKSPACE#ID#<workspaceId>` | `CONFIGURATION#<normalizedConfigName>#<configurationId>` |\n *\n * The PK co-locates with the canonical Workspace record\n * (`SK = CURRENT`) and the Membership / RoleAssignment workspace-\n * projections (patterns #2, #9), so an admin workspace dashboard can\n * hydrate workspace metadata + member projections + role-assignment\n * projections + workspace-scoped Configurations in a single `Query`.\n *\n * `<normalizedConfigName>` derives from Configuration's `key` attribute\n * (the canonical name dimension — Configuration carries no `displayName`\n * per TR-024 § Open Item #5, so `key` is the natural sort source). The\n * SK shape is operation-owned: the operations-layer projection writer\n * composes the SK string via `buildConfigurationWorkspaceProjectionSk`\n * and supplies it on the `sk` attribute. This entity stores the SK\n * verbatim — no `watch`/derived computation here — so the SK grammar\n * (and any future revision) lives in one place: the operations layer.\n *\n * Projection attribute set per ADR-018 § Projection attribute set and\n * the implementation guide § 2: `summary`, `vid`, `lastUpdated` (so\n * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'CONFIGURATION#')`\n * is self-sufficient — no BatchGet hop to the canonical record), plus\n * the projection-discriminating fields (`configurationId`, `workspaceId`,\n * `tenantId`, `scope`).\n *\n * **Tenant-prefixed partition.** Unlike the Configuration user-\n * projection (whose PK is `USER#ID#<userId>` with no tenant prefix —\n * a user's user-scoped Configurations are cross-tenant by design),\n * the workspace-projection PK carries the tenant prefix because\n * Workspaces are tenant-scoped per ADR-011. This mirrors the\n * Membership / RoleAssignment workspace-projection partitions.\n *\n * **No GSI projection.** Per ADR-018 § Decision, cross-cutting reads\n * are served by the main-table partition\n * `TID#<tenantId>#WORKSPACE#ID#<workspaceId>`; the GSI1/GSI2 catalog\n * is unchanged. Tenant-scoped Configurations continue to use the\n * canonical GSI1 path (ADR-011) unchanged.\n *\n * @see ADR-018 § Access Pattern Coverage (#10 — workspace-scope half)\n * @see .state/adr-018-implementation-guide.md § 1 (SK grammar) and § 2 (attribute set)\n * @see .claude/rules/data-layer-layout.md — projection writers live in operations, not here\n */\nexport const ConfigurationWorkspaceProjectionEntity = new Entity({\n model: {\n entity: \"configurationWorkspaceProjection\",\n service: \"control\",\n version: \"01\",\n },\n attributes: {\n /**\n * Tenant the workspace belongs to. Renders as the leading segment\n * of the base-table PK. Always required — the workspace partition\n * is tenant-scoped per ADR-011.\n */\n tenantId: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Workspace partition discriminator. Renders as the trailing\n * segment of the base-table PK\n * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —\n * the projection has no meaning outside a workspace partition.\n */\n workspaceId: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Pre-composed sort key — built by the operations-layer projection\n * writer via `buildConfigurationWorkspaceProjectionSk`. The entity\n * stores the value verbatim so the SK grammar (pattern #10\n * workspace-scope) is owned by the operations layer, not\n * duplicated here.\n */\n sk: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Configuration canonical-record id. Stored as a discriminating\n * field so consumers can hydrate the canonical row via the\n * Configuration get-by-id operation when the projection's `summary`\n * is insufficient.\n */\n configurationId: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Scope marker. Always `\"workspace\"` on this projection — recorded\n * explicitly so future scope-bearing projections (user, tenant,\n * role) can share filter semantics in a unified cross-projection\n * list query if one ever lands.\n */\n scope: {\n type: \"string\" as const,\n required: true,\n default: \"workspace\",\n },\n /**\n * Configuration's `key` attribute (config category, e.g. endpoints,\n * branding, display). Mirrored from the canonical row so consumers\n * reading the projection get the natural display label without a\n * BatchGet hop. Doubles as the source of `<normalizedConfigName>`\n * in the SK.\n */\n displayName: {\n type: \"string\" as const,\n required: false,\n },\n /**\n * Summary projection (key display fields as JSON string) — mirrored\n * from the canonical Configuration row so workspace-partition\n * queries do not need a BatchGet hop.\n */\n summary: {\n type: \"string\" as const,\n required: true,\n },\n /** Version id mirrored from the canonical Configuration row. */\n vid: {\n type: \"string\" as const,\n required: true,\n },\n /** Last-updated timestamp mirrored from the canonical Configuration row. */\n lastUpdated: {\n type: \"string\" as const,\n required: true,\n },\n },\n indexes: {\n /**\n * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,\n * SK = operation-supplied. A single\n * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'CONFIGURATION#')`\n * returns the workspace's workspace-scoped Configurations sorted by\n * `<normalizedConfigName>` (then `<configurationId>` as the\n * tiebreaker).\n */\n record: {\n pk: {\n field: \"PK\",\n composite: [\"tenantId\", \"workspaceId\"],\n template: \"TID#${tenantId}#WORKSPACE#ID#${workspaceId}\",\n },\n sk: {\n field: \"SK\",\n casing: \"none\" as const,\n composite: [\"sk\"],\n template: \"${sk}\",\n },\n },\n },\n});\n","import { Entity } from \"electrodb\";\nimport {\n gsi1ShardAttribute,\n membershipGsi1skAttribute,\n} from \"./control-entity-common\";\n\n/**\n * Membership data-store entity (single-table store).\n *\n * **Classification (ADR 2026-03-03-01):** Tenant-isolated, control plane. Membership links a User\n * to a Tenant (and optionally a Workspace). One record per (tenantId, id).\n *\n * Key structure: PK = TID#<tenantId>#MEMBERSHIP#ID#<id>, SK = CURRENT.\n * Uniqueness: one Membership per (tenantId, id).\n *\n * GSI1 — Unified Sharded List per ADR-011: lists all Memberships in a tenant across the four\n * shards. Membership is tenant-scoped (not workspace-scoped), so the GSI1 PK uses `WID#-` as a\n * sentinel.\n *\n * @see sites/www-docs/content/architecture/adr/2026-03-03-01-tenant-isolated-vs-non-tenant-isolated-entities.md\n * @see sites/www-docs/content/architecture/adr/2026-03-13-02-control-plane-roles-and-user-tenant-workspace-linkage.md\n * @see sites/www-docs/content/packages/@openhi/constructs/data/dynamo/single-table-design.md\n */\nexport const MembershipEntity = new Entity({\n model: {\n entity: \"membership\",\n service: \"control\",\n version: \"01\",\n },\n attributes: {\n /** Sort key sentinel. Always \"CURRENT\". */\n sk: {\n type: \"string\" as const,\n required: true,\n default: \"CURRENT\",\n },\n /** Tenant in which the user has membership (required). */\n tenantId: {\n type: \"string\" as const,\n required: true,\n },\n /** FHIR Resource.id; membership id. */\n id: {\n type: \"string\" as const,\n required: true,\n },\n /** Full Membership resource serialized as JSON string. */\n resource: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Summary projection (key display fields as JSON string: id, displayName, status).\n * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.\n */\n summary: {\n type: \"string\" as const,\n required: true,\n },\n /** Version id (e.g. ULID). */\n vid: {\n type: \"string\" as const,\n required: true,\n },\n lastUpdated: {\n type: \"string\" as const,\n required: true,\n },\n gsi1Shard: gsi1ShardAttribute,\n /**\n * Derived GSI1 sort key — `<normalizedUserName>#<id>` per ADR-018\n * pattern #1 so a GSI1 query partitioned on the tenant range-scans\n * by user-name prefix and returns memberships sorted by user name.\n * Falls back to `<lastUpdated>#<id>` when `denormalizedUserName`\n * is missing.\n */\n gsi1sk: membershipGsi1skAttribute,\n deleted: {\n type: \"boolean\" as const,\n required: false,\n },\n bundleId: {\n type: \"string\" as const,\n required: false,\n },\n msgId: {\n type: \"string\" as const,\n required: false,\n },\n /**\n * Denormalized `linked-data-identity` Reference (e.g. `Practitioner/abc`).\n * Populated from the FHIR extension on the Membership resource at write\n * time so future GSIs can index data-plane identity lookups without\n * deserializing the full resource JSON. See ADR 2026-03-13-02 §6.\n */\n linkedDataIdentityRef: {\n type: \"string\" as const,\n required: false,\n },\n /**\n * Denormalized display name of the linked Tenant, captured at row\n * last-write time. Promoted to a top-level attribute so the ADR-018\n * adjacency-list projection SKs (pattern #3 — `MEMBERSHIP#TENANT#<normalizedTenantName>#…`)\n * can be composed from a top-level field instead of digging into the\n * `resource` JSON. Optional on the schema so pre-TR-024 rows do not\n * break; the operations-layer multi-write helper (#1010) makes the\n * field load-bearing at write time per TR-024 rule 2 (write-time\n * source = canonical Tenant.displayName).\n * @see TR-024 — Denormalized display-name attributes\n */\n denormalizedTenantName: {\n type: \"string\" as const,\n required: false,\n },\n /**\n * Denormalized display name of the linked User, captured at row\n * last-write time. Promoted to a top-level attribute so the ADR-018\n * adjacency-list canonical-record GSI1SK (pattern #1 —\n * `<normalizedUserName>#<id>`) and workspace-projection SK (pattern #2)\n * can be composed from a top-level field. Optional on the schema so\n * pre-TR-024 rows do not break; the operations-layer multi-write helper\n * (#1010) makes the field load-bearing at write time per TR-024 rule 2\n * (write-time source = canonical User.displayName).\n * @see TR-024 — Denormalized display-name attributes\n */\n denormalizedUserName: {\n type: \"string\" as const,\n required: false,\n },\n },\n indexes: {\n /** Base table: PK = TID#<tenantId>#MEMBERSHIP#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */\n record: {\n pk: {\n field: \"PK\",\n composite: [\"tenantId\", \"id\"],\n template: \"TID#${tenantId}#MEMBERSHIP#ID#${id}\",\n },\n sk: {\n field: \"SK\",\n composite: [\"sk\"],\n template: \"${sk}\",\n },\n },\n\n /**\n * GSI1 — Unified Sharded List per ADR-011: list all Memberships for a tenant across the\n * four shards. Membership is tenant-scoped only, so `WID#-` is a sentinel.\n * SK is derived via `membershipGsi1skAttribute` — composes\n * `<normalizedUserName>#<id>` per ADR-018 pattern #1 (users in a\n * tenant, sorted by user name); falls back to `<lastUpdated>#<id>`\n * when `denormalizedUserName` is missing. `casing: \"none\"` preserves\n * the normalized label and ISO-8601 `T`/`Z`.\n */\n gsi1: {\n index: \"GSI1\",\n pk: {\n field: \"GSI1PK\",\n composite: [\"tenantId\", \"gsi1Shard\"],\n template: \"TID#${tenantId}#WID#-#RT#Membership#SHARD#${gsi1Shard}\",\n },\n sk: {\n field: \"GSI1SK\",\n casing: \"none\" as const,\n composite: [\"gsi1sk\"],\n template: \"${gsi1sk}\",\n },\n },\n },\n});\n","import { Entity } from \"electrodb\";\n\n/**\n * Membership user-projection entity (single-table store, no GSI).\n *\n * **ADR-018 adjacency-list projection.** For every Membership write the\n * operations-layer multi-write helper writes one of two projection rows\n * under the user partition so the user-rooted access patterns #3 and #4\n * are served by a single base-table `Query` with no GSI hop:\n *\n * | Pattern | When | PK | SK |\n * |---|---|---|---|\n * | #3 — tenant sub-lane | `workspaceId` absent | `USER#ID#<userId>` | `MEMBERSHIP#TENANT#<normalizedTenantName>#TID#<tenantId>#<id>` |\n * | #4 — workspace sub-lane | `workspaceId` set | `USER#ID#<userId>` | `MEMBERSHIP#WORKSPACE#TID#<tenantId>#<normalizedWorkspaceName>#WID#<workspaceId>#<id>` |\n *\n * Both shapes share the user-partition `PK = USER#ID#<userId>`. The SK\n * shape is operation-owned: the operations-layer projection writer\n * composes the SK string via the `buildMembershipUserProjectionSk*`\n * helpers and supplies it on the `sk` attribute. This entity stores the\n * SK verbatim — no `watch`/derived computation here — so the SK grammar\n * (and any future revision) lives in one place: the operations layer.\n *\n * Projection attribute set per ADR-018 § Projection attribute set and\n * the implementation guide § 2: `summary`, `vid`, `lastUpdated` (so\n * `Query(PK = USER#ID#<userId>, SK begins_with 'MEMBERSHIP#')` is\n * self-sufficient — no BatchGet hop to the canonical record), plus the\n * projection-discriminating fields (`tenantId`, `userId`, `workspaceId?`,\n * `membershipId`) and TR-024 denormalized display names\n * (`denormalizedTenantName`, `denormalizedUserName`,\n * `denormalizedWorkspaceName?`).\n *\n * **No GSI projection.** Per ADR-018 § Decision, cross-cutting reads\n * are served by the main-table partition `USER#ID#<userId>`; the\n * GSI1/GSI2 catalog is unchanged.\n *\n * @see ADR-018 § Access Pattern Coverage (#3, #4)\n * @see .state/adr-018-implementation-guide.md § 1 (SK grammar) and § 2 (attribute set)\n * @see .claude/rules/data-layer-layout.md — projection writers live in operations, not here\n */\nexport const MembershipUserProjectionEntity = new Entity({\n model: {\n entity: \"membershipUserProjection\",\n service: \"control\",\n version: \"01\",\n },\n attributes: {\n /**\n * User partition discriminator. Renders as `USER#ID#<userId>` on the\n * base-table PK. Always required — the projection has no meaning\n * outside a user partition.\n */\n userId: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Pre-composed sort key — built by the operations-layer projection\n * writer via `buildMembershipUserProjectionSk*` helpers. The entity\n * stores the value verbatim so the SK grammar (patterns #3 and #4)\n * is owned by the operations layer, not duplicated here.\n */\n sk: {\n type: \"string\" as const,\n required: true,\n },\n /** Tenant in which the membership applies. Always required. */\n tenantId: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Workspace the membership scopes to. Present iff the projection\n * row is a pattern-#4 workspace sub-lane row; absent for pattern-#3\n * tenant sub-lane rows.\n */\n workspaceId: {\n type: \"string\" as const,\n required: false,\n },\n /**\n * Membership canonical-record id. Stored as a discriminating field\n * so consumers can hydrate the canonical row via\n * `MembershipEntity.get({ tenantId, id: membershipId })` when the\n * projection's `summary` is insufficient.\n */\n membershipId: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Summary projection (key display fields as JSON string: id,\n * displayName, status) — mirrored from the canonical Membership row\n * so user-partition queries do not need a BatchGet hop.\n */\n summary: {\n type: \"string\" as const,\n required: true,\n },\n /** Version id mirrored from the canonical Membership row. */\n vid: {\n type: \"string\" as const,\n required: true,\n },\n /** Last-updated timestamp mirrored from the canonical Membership row. */\n lastUpdated: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Denormalized Tenant display name — required to compose pattern-#3\n * SK (`MEMBERSHIP#TENANT#<normalizedTenantName>#…`). Optional on the\n * schema because pre-TR-024 rows may not carry a display name; the\n * operations layer falls back gracefully when missing.\n */\n denormalizedTenantName: {\n type: \"string\" as const,\n required: false,\n },\n /**\n * Denormalized User display name — mirrored from the canonical\n * Membership row per TR-024 rule 3 (canonical-record symmetry).\n * Carried on the projection so consumers can render the user's\n * display name without a hop to the User record.\n */\n denormalizedUserName: {\n type: \"string\" as const,\n required: false,\n },\n /**\n * Denormalized Workspace display name — required to compose\n * pattern-#4 SK (`MEMBERSHIP#WORKSPACE#TID#<tenantId>#<normalizedWorkspaceName>#…`).\n * Optional on the schema (TR-024 § Open Item #4 defers a formal\n * Workspace-rename cascade); the operations layer falls back to a\n * sentinel when missing so the SK still has a valid shape.\n */\n denormalizedWorkspaceName: {\n type: \"string\" as const,\n required: false,\n },\n },\n indexes: {\n /**\n * Base table: PK = USER#ID#\\<userId\\>, SK = operation-supplied.\n * Both pattern #3 and pattern #4 use this same index — the SK string\n * encodes the lane discriminator (`MEMBERSHIP#TENANT#…` vs\n * `MEMBERSHIP#WORKSPACE#…`) so a single\n * `Query(PK = USER#ID#<userId>, SK begins_with 'MEMBERSHIP#')`\n * returns both lanes interleaved.\n */\n record: {\n pk: {\n field: \"PK\",\n composite: [\"userId\"],\n template: \"USER#ID#${userId}\",\n },\n sk: {\n field: \"SK\",\n casing: \"none\" as const,\n composite: [\"sk\"],\n template: \"${sk}\",\n },\n },\n },\n});\n","import { Entity } from \"electrodb\";\n\n/**\n * Membership workspace-projection entity (single-table store, no GSI).\n *\n * **ADR-018 adjacency-list projection.** For every workspace-scoped\n * Membership the operations-layer multi-write helper writes one\n * projection row under the workspace partition so the workspace-rooted\n * access pattern #2 is served by a single base-table `Query` with no\n * GSI hop:\n *\n * | Pattern | When | PK | SK |\n * |---|---|---|---|\n * | #2 — users in a workspace | `workspaceId` set | `TID#<tenantId>#WORKSPACE#ID#<workspaceId>` | `MEMBERSHIP#<normalizedUserName>#USER#<userId>#<id>` |\n *\n * The PK co-locates with the canonical Workspace record\n * (`SK = CURRENT`) so a single `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>)`\n * returns workspace metadata + every member projection in one round\n * trip. The SK shape is operation-owned: the operations-layer\n * projection writer composes the SK string via the\n * `buildMembershipWorkspaceProjectionSk` helper and supplies it on the\n * `sk` attribute. This entity stores the SK verbatim — no\n * `watch`/derived computation here — so the SK grammar lives in one\n * place: the operations layer.\n *\n * Projection attribute set per ADR-018 § Projection attribute set and\n * the implementation guide § 2: `summary`, `vid`, `lastUpdated` (so\n * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'MEMBERSHIP#')`\n * is self-sufficient — no BatchGet hop to the canonical record), plus\n * the projection-discriminating fields (`tenantId`, `workspaceId`,\n * `userId`, `membershipId`) and TR-024 denormalized user display name\n * (`denormalizedUserName`).\n *\n * **No GSI projection.** Per ADR-018 § Decision, cross-cutting reads\n * are served by the main-table partition\n * `TID#<tenantId>#WORKSPACE#ID#<workspaceId>`; the GSI1/GSI2 catalog\n * is unchanged.\n *\n * @see ADR-018 § Access Pattern Coverage (#2)\n * @see .state/adr-018-implementation-guide.md § 1 (SK grammar) and § 2 (attribute set)\n * @see .claude/rules/data-layer-layout.md — projection writers live in operations, not here\n */\nexport const MembershipWorkspaceProjectionEntity = new Entity({\n model: {\n entity: \"membershipWorkspaceProjection\",\n service: \"control\",\n version: \"01\",\n },\n attributes: {\n /**\n * Tenant the workspace belongs to. Renders as the leading segment\n * of the base-table PK. Always required — the workspace partition\n * is tenant-scoped per ADR-011.\n */\n tenantId: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Workspace partition discriminator. Renders as the trailing\n * segment of the base-table PK\n * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —\n * the projection has no meaning outside a workspace partition.\n */\n workspaceId: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Pre-composed sort key — built by the operations-layer projection\n * writer via `buildMembershipWorkspaceProjectionSk`. The entity\n * stores the value verbatim so the SK grammar (pattern #2) is\n * owned by the operations layer, not duplicated here.\n */\n sk: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * User the membership links. Stored as a discriminating field so\n * consumers can hydrate the canonical User row via\n * `UserEntity.get({ id: userId, sk: \"CURRENT\" })` when the\n * projection's `summary` is insufficient.\n */\n userId: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Membership canonical-record id. Stored as a discriminating field\n * so consumers can hydrate the canonical row via\n * `MembershipEntity.get({ tenantId, id: membershipId })` when the\n * projection's `summary` is insufficient.\n */\n membershipId: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Summary projection (key display fields as JSON string: id,\n * displayName, status) — mirrored from the canonical Membership row\n * so workspace-partition queries do not need a BatchGet hop.\n */\n summary: {\n type: \"string\" as const,\n required: true,\n },\n /** Version id mirrored from the canonical Membership row. */\n vid: {\n type: \"string\" as const,\n required: true,\n },\n /** Last-updated timestamp mirrored from the canonical Membership row. */\n lastUpdated: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Denormalized User display name — required to compose the\n * pattern-#2 SK (`MEMBERSHIP#<normalizedUserName>#…`). Optional on\n * the schema because pre-TR-024 rows may not carry a display name;\n * the operations layer falls back to a sentinel when missing so\n * the SK still has a valid shape. The TR-023 rename-cascade\n * pipeline rewrites the SK on a User rename.\n */\n denormalizedUserName: {\n type: \"string\" as const,\n required: false,\n },\n },\n indexes: {\n /**\n * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,\n * SK = operation-supplied. Pattern #2 uses this index — the SK\n * encodes the entity-type prefix (`MEMBERSHIP#…`) so a\n * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'MEMBERSHIP#')`\n * returns every member projection for the workspace in normalized-\n * user-name sort order.\n */\n record: {\n pk: {\n field: \"PK\",\n composite: [\"tenantId\", \"workspaceId\"],\n template: \"TID#${tenantId}#WORKSPACE#ID#${workspaceId}\",\n },\n sk: {\n field: \"SK\",\n casing: \"none\" as const,\n composite: [\"sk\"],\n template: \"${sk}\",\n },\n },\n },\n});\n","import { Entity } from \"electrodb\";\nimport { gsi1ShardAttribute, gsi1skAttribute } from \"./control-entity-common\";\n\n/**\n * Role data-store entity (single-table store).\n *\n * **Classification (ADR 2026-03-03-01):** Non-tenant-isolated, control plane. Role is a\n * platform-wide role catalog (e.g. tenant-admin, tenant-user, system-admin); not scoped by tenant.\n * RoleAssignment references Role to assign a role to a User in a Tenant/Workspace context.\n *\n * Key structure: PK = ROLE#ID#<id>, SK = CURRENT.\n * The ROLE# prefix prevents key collisions with other non-tenant-isolated entities (User, etc.)\n * sharing the same table (ADR 2026-03-11-01 — preferred pattern for all control plane entities).\n * Uniqueness: one Role per id.\n *\n * GSI1 — Unified Sharded List per ADR-011: lists all Roles across the four shards. Non-tenant-\n * isolated, so the PK uses `TID#-#WID#-` sentinels.\n *\n * @see sites/www-docs/content/architecture/adr/2026-03-03-01-tenant-isolated-vs-non-tenant-isolated-entities.md\n * @see sites/www-docs/content/architecture/adr/2026-03-13-02-control-plane-roles-and-user-tenant-workspace-linkage.md\n * @see sites/www-docs/content/packages/@openhi/constructs/data/dynamo/single-table-design.md\n */\nexport const RoleEntity = new Entity({\n model: {\n entity: \"role\",\n service: \"control\",\n version: \"01\",\n },\n attributes: {\n /** Sort key sentinel. Always \"CURRENT\". */\n sk: {\n type: \"string\" as const,\n required: true,\n default: \"CURRENT\",\n },\n /** FHIR Resource.id; role id. */\n id: {\n type: \"string\" as const,\n required: true,\n },\n /** Full Role resource serialized as JSON string. */\n resource: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Summary projection (key display fields as JSON string: id, displayName, status).\n * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.\n */\n summary: {\n type: \"string\" as const,\n required: true,\n },\n /** Version id (e.g. ULID). */\n vid: {\n type: \"string\" as const,\n required: true,\n },\n lastUpdated: {\n type: \"string\" as const,\n required: true,\n },\n gsi1Shard: gsi1ShardAttribute,\n /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */\n gsi1sk: gsi1skAttribute,\n deleted: {\n type: \"boolean\" as const,\n required: false,\n },\n bundleId: {\n type: \"string\" as const,\n required: false,\n },\n msgId: {\n type: \"string\" as const,\n required: false,\n },\n },\n indexes: {\n /** Base table: PK = ROLE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */\n record: {\n pk: {\n field: \"PK\",\n composite: [\"id\"],\n template: \"ROLE#ID#${id}\",\n },\n sk: {\n field: \"SK\",\n composite: [\"sk\"],\n template: \"${sk}\",\n },\n },\n\n /**\n * GSI1 — Unified Sharded List per ADR-011: list all Roles across the four shards.\n * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#Role#SHARD#<n>`.\n * SK is derived via `gsi1skAttribute` — uses the resource's natural label when\n * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: \"none\"` preserves the\n * normalized label and ISO-8601 `T`/`Z`.\n */\n gsi1: {\n index: \"GSI1\",\n pk: {\n field: \"GSI1PK\",\n composite: [\"gsi1Shard\"],\n template: \"TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}\",\n },\n sk: {\n field: \"GSI1SK\",\n casing: \"none\" as const,\n composite: [\"gsi1sk\"],\n template: \"${gsi1sk}\",\n },\n },\n },\n});\n","import { Entity } from \"electrodb\";\nimport {\n gsi1ShardAttribute,\n roleAssignmentGsi1skAttribute,\n} from \"./control-entity-common\";\n\n/**\n * RoleAssignment data-store entity (single-table store).\n *\n * **Classification (ADR 2026-03-03-01):** Tenant-isolated, control plane. RoleAssignment assigns\n * a Role to a User in a Tenant (and optionally Workspace) context.\n *\n * Key structure: PK = TID#<tenantId>#ROLEASSIGNMENT#ID#<id>, SK = CURRENT.\n * Uniqueness: one RoleAssignment per (tenantId, id).\n *\n * GSI1 — Unified Sharded List per ADR-011: lists all RoleAssignments in a tenant across the four\n * shards. Tenant-scoped only (workspace context lives inside the resource), so the GSI1 PK uses\n * `WID#-` as a sentinel.\n *\n * @see sites/www-docs/content/architecture/adr/2026-03-03-01-tenant-isolated-vs-non-tenant-isolated-entities.md\n * @see sites/www-docs/content/architecture/adr/2026-03-13-02-control-plane-roles-and-user-tenant-workspace-linkage.md\n * @see sites/www-docs/content/packages/@openhi/constructs/data/dynamo/single-table-design.md\n */\nexport const RoleAssignmentEntity = new Entity({\n model: {\n entity: \"roleassignment\",\n service: \"control\",\n version: \"01\",\n },\n attributes: {\n /** Sort key sentinel. Always \"CURRENT\". */\n sk: {\n type: \"string\" as const,\n required: true,\n default: \"CURRENT\",\n },\n /** Tenant in which the role assignment applies (required). */\n tenantId: {\n type: \"string\" as const,\n required: true,\n },\n /** FHIR Resource.id; role assignment id. */\n id: {\n type: \"string\" as const,\n required: true,\n },\n /** Full RoleAssignment resource serialized as JSON string. */\n resource: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Summary projection (key display fields as JSON string: id, displayName, status).\n * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.\n */\n summary: {\n type: \"string\" as const,\n required: true,\n },\n /** Version id (e.g. ULID). */\n vid: {\n type: \"string\" as const,\n required: true,\n },\n lastUpdated: {\n type: \"string\" as const,\n required: true,\n },\n gsi1Shard: gsi1ShardAttribute,\n /**\n * Derived GSI1 sort key — discriminator-first\n * `<roleId>#<normalizedUserName>#<id>` per ADR-018 pattern #8 so a\n * GSI1 query partitioned on the tenant can `begins_with('<roleId>#')`\n * to enumerate every user assigned to a given role, sorted by user\n * name. Falls back to `<lastUpdated>#<id>` when either component is\n * missing.\n */\n gsi1sk: roleAssignmentGsi1skAttribute,\n deleted: {\n type: \"boolean\" as const,\n required: false,\n },\n bundleId: {\n type: \"string\" as const,\n required: false,\n },\n msgId: {\n type: \"string\" as const,\n required: false,\n },\n /**\n * Denormalized display name of the linked Tenant, captured at row\n * last-write time. Promoted to a top-level attribute so the ADR-018\n * adjacency-list user-projection SK (pattern #5 —\n * `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#<roleId>#TID#<tenantId>#<id>`)\n * can be composed from a top-level field instead of digging into the\n * `resource` JSON. Optional on the schema so pre-TR-024 rows do not\n * break; the operations-layer multi-write helper (#1010) makes the\n * field load-bearing at write time per TR-024 rule 2 (write-time\n * source = canonical Tenant.displayName).\n * @see TR-024 — Denormalized display-name attributes\n */\n denormalizedTenantName: {\n type: \"string\" as const,\n required: false,\n },\n /**\n * Denormalized display name of the linked User, captured at row\n * last-write time. Promoted to a top-level attribute so the ADR-018\n * adjacency-list canonical-record GSI1SK (pattern #8 —\n * `<roleId>#<normalizedUserName>#<id>`) and workspace-projection SK\n * (pattern #9) can be composed from a top-level field. Optional on\n * the schema so pre-TR-024 rows do not break; the operations-layer\n * multi-write helper (#1010) makes the field load-bearing at write\n * time per TR-024 rule 2 (write-time source = canonical\n * User.displayName).\n * @see TR-024 — Denormalized display-name attributes\n */\n denormalizedUserName: {\n type: \"string\" as const,\n required: false,\n },\n /**\n * Denormalized display name of the linked Role, captured at row\n * last-write time. Promoted to a top-level attribute so the ADR-018\n * adjacency-list user-projection SK (pattern #5 —\n * `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#…`) can be composed from\n * a top-level field. Optional on the schema so pre-TR-024 rows do not\n * break; the operations-layer multi-write helper (#1010) makes the\n * field load-bearing at write time per TR-024 rule 2 (write-time\n * source = canonical Role.displayName).\n * @see TR-024 — Denormalized display-name attributes\n */\n denormalizedRoleName: {\n type: \"string\" as const,\n required: false,\n },\n },\n indexes: {\n /** Base table: PK = TID#<tenantId>#ROLEASSIGNMENT#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */\n record: {\n pk: {\n field: \"PK\",\n composite: [\"tenantId\", \"id\"],\n template: \"TID#${tenantId}#ROLEASSIGNMENT#ID#${id}\",\n },\n sk: {\n field: \"SK\",\n composite: [\"sk\"],\n template: \"${sk}\",\n },\n },\n\n /**\n * GSI1 — Unified Sharded List per ADR-011: list all RoleAssignments for a tenant across the\n * four shards. Tenant-scoped only, so `WID#-` is a sentinel.\n * SK is derived via `roleAssignmentGsi1skAttribute` — composes the\n * discriminator-first `<roleId>#<normalizedUserName>#<id>` shape per\n * ADR-018 pattern #8 (users with a specific role in a tenant, sorted\n * by user name); falls back to `<lastUpdated>#<id>` when either\n * component is missing. `casing: \"none\"` preserves the normalized\n * label and ISO-8601 `T`/`Z`.\n */\n gsi1: {\n index: \"GSI1\",\n pk: {\n field: \"GSI1PK\",\n composite: [\"tenantId\", \"gsi1Shard\"],\n template: \"TID#${tenantId}#WID#-#RT#RoleAssignment#SHARD#${gsi1Shard}\",\n },\n sk: {\n field: \"GSI1SK\",\n casing: \"none\" as const,\n composite: [\"gsi1sk\"],\n template: \"${gsi1sk}\",\n },\n },\n },\n});\n","import { Entity } from \"electrodb\";\n\n/**\n * RoleAssignment user-projection entity (single-table store, no GSI).\n *\n * **ADR-018 adjacency-list projection.** For every RoleAssignment write\n * the operations-layer multi-write helper writes one projection row\n * under the user partition so the user-rooted access pattern #5 is\n * served by a single base-table `Query` with no GSI hop. The SK encodes\n * a tenant-vs-workspace discriminator sub-prefix so both sub-lanes share\n * the user partition:\n *\n * | Sub-lane | When | PK | SK |\n * |---|---|---|---|\n * | tenant-level | `workspaceId` absent | `USER#ID#<userId>` | `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#<roleId>#TID#<tenantId>#<id>` |\n * | workspace-level | `workspaceId` set | `USER#ID#<userId>` | `ROLEASSIGNMENT#WORKSPACE#<normalizedRoleName>#<roleId>#TID#<tenantId>#WID#<workspaceId>#<id>` |\n *\n * The SK shape is operation-owned: the operations-layer projection\n * writer composes the SK string via the\n * `buildRoleAssignmentUserProjectionSk*` helpers and supplies it on the\n * `sk` attribute. This entity stores the SK verbatim — no\n * `watch`/derived computation here — so the SK grammar (and any future\n * revision) lives in one place: the operations layer.\n *\n * Projection attribute set per ADR-018 § Projection attribute set and\n * the implementation guide § 2: `summary`, `vid`, `lastUpdated` (so\n * `Query(PK = USER#ID#<userId>, SK begins_with 'ROLEASSIGNMENT#')` is\n * self-sufficient — no BatchGet hop to the canonical record), plus the\n * projection-discriminating fields (`tenantId`, `roleId`,\n * `roleAssignmentId`, `userId`, `workspaceId?`) and TR-024 denormalized\n * display names (`denormalizedTenantName`, `denormalizedUserName`,\n * `denormalizedRoleName`).\n *\n * **No GSI projection.** Per ADR-018 § Decision, cross-cutting reads\n * are served by the main-table partition `USER#ID#<userId>`; the\n * GSI1/GSI2 catalog is unchanged.\n *\n * @see ADR-018 § Access Pattern Coverage (#5)\n * @see .state/adr-018-implementation-guide.md § 1 (SK grammar) and § 2 (attribute set)\n * @see .claude/rules/data-layer-layout.md — projection writers live in operations, not here\n */\nexport const RoleAssignmentUserProjectionEntity = new Entity({\n model: {\n entity: \"roleAssignmentUserProjection\",\n service: \"control\",\n version: \"01\",\n },\n attributes: {\n /**\n * User partition discriminator. Renders as `USER#ID#<userId>` on the\n * base-table PK. Always required — the projection has no meaning\n * outside a user partition.\n */\n userId: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Pre-composed sort key — built by the operations-layer projection\n * writer via `buildRoleAssignmentUserProjectionSk*` helpers. The\n * entity stores the value verbatim so the SK grammar (tenant-lane\n * vs workspace-lane) is owned by the operations layer, not\n * duplicated here.\n */\n sk: {\n type: \"string\" as const,\n required: true,\n },\n /** Tenant in which the role assignment applies. Always required. */\n tenantId: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Workspace the role assignment scopes to. Present iff the\n * projection row is the workspace-level sub-lane; absent for\n * tenant-level sub-lane rows.\n */\n workspaceId: {\n type: \"string\" as const,\n required: false,\n },\n /**\n * Role the assignment grants. Stored as a discriminating field so\n * `Query(PK = USER#ID#<userId>, SK begins_with 'ROLEASSIGNMENT#…')`\n * results carry the role id without a hop to the canonical row.\n */\n roleId: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * RoleAssignment canonical-record id. Stored as a discriminating\n * field so consumers can hydrate the canonical row via\n * `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`\n * when the projection's `summary` is insufficient.\n */\n roleAssignmentId: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Summary projection (key display fields as JSON string: id,\n * displayName, status) — mirrored from the canonical RoleAssignment\n * row so user-partition queries do not need a BatchGet hop.\n */\n summary: {\n type: \"string\" as const,\n required: true,\n },\n /** Version id mirrored from the canonical RoleAssignment row. */\n vid: {\n type: \"string\" as const,\n required: true,\n },\n /** Last-updated timestamp mirrored from the canonical RoleAssignment row. */\n lastUpdated: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Denormalized Tenant display name — mirrored from the canonical\n * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).\n * Optional on the schema because pre-TR-024 rows may not carry a\n * display name; the operations layer falls back gracefully when\n * missing.\n */\n denormalizedTenantName: {\n type: \"string\" as const,\n required: false,\n },\n /**\n * Denormalized User display name — mirrored from the canonical\n * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).\n * Carried on the projection so consumers can render the user's\n * display name without a hop to the User record.\n */\n denormalizedUserName: {\n type: \"string\" as const,\n required: false,\n },\n /**\n * Denormalized Role display name — required to compose the SK's\n * `<normalizedRoleName>` segment. Optional on the schema (pre-TR-024\n * rows fall back to a sentinel) but expected to be present at write\n * time per TR-024 rule 2 (write-time source =\n * canonical Role.displayName).\n */\n denormalizedRoleName: {\n type: \"string\" as const,\n required: false,\n },\n },\n indexes: {\n /**\n * Base table: PK = USER#ID#\\<userId\\>, SK = operation-supplied. Both\n * sub-lanes (tenant-level and workspace-level) use this same index —\n * the SK string encodes the lane discriminator\n * (`ROLEASSIGNMENT#TENANT#…` vs `ROLEASSIGNMENT#WORKSPACE#…`) so a\n * single `Query(PK = USER#ID#<userId>, SK begins_with 'ROLEASSIGNMENT#')`\n * returns both lanes interleaved.\n */\n record: {\n pk: {\n field: \"PK\",\n composite: [\"userId\"],\n template: \"USER#ID#${userId}\",\n },\n sk: {\n field: \"SK\",\n casing: \"none\" as const,\n composite: [\"sk\"],\n template: \"${sk}\",\n },\n },\n },\n});\n","import { Entity } from \"electrodb\";\n\n/**\n * RoleAssignment workspace-projection entity (single-table store, no GSI).\n *\n * **ADR-018 adjacency-list projection.** For every workspace-scoped\n * RoleAssignment the operations-layer multi-write helper writes one\n * projection row under the workspace partition so the workspace-rooted\n * access pattern #9 is served by a single base-table `Query` with no\n * GSI hop:\n *\n * | Pattern | When | PK | SK |\n * |---|---|---|---|\n * | #9 — users with a specific role in a workspace | `workspaceId` set | `TID#<tenantId>#WORKSPACE#ID#<workspaceId>` | `ROLEASSIGNMENT#<roleId>#<normalizedUserName>#USER#<userId>#<id>` |\n *\n * The SK is **discriminator-first** on the raw `<roleId>` (mirroring the\n * canonical GSI1SK from pattern #8): role id discriminates first so a\n * `begins_with('ROLEASSIGNMENT#<roleId>#')` filter returns every user\n * assigned to that role in the workspace, sorted alphabetically by\n * normalized user name. Omitting the `<roleId>#` segment\n * (`begins_with('ROLEASSIGNMENT#')`) returns every role assignment in\n * the workspace interleaved.\n *\n * The PK co-locates with the canonical Workspace record (`SK = CURRENT`)\n * and the Membership workspace-projection rows (pattern #2) so a single\n * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>)` returns\n * workspace metadata + every member projection + every role-assignment\n * projection in one round trip — the admin workspace-dashboard read shape.\n *\n * The SK shape is operation-owned: the operations-layer projection\n * writer composes the SK string via the\n * `buildRoleAssignmentWorkspaceProjectionSk` helper and supplies it on\n * the `sk` attribute. This entity stores the SK verbatim — no\n * `watch`/derived computation here — so the SK grammar lives in one\n * place: the operations layer.\n *\n * Projection attribute set per ADR-018 § Projection attribute set and\n * the implementation guide § 2: `summary`, `vid`, `lastUpdated` (so\n * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'ROLEASSIGNMENT#')`\n * is self-sufficient — no BatchGet hop to the canonical record), plus\n * the projection-discriminating fields (`tenantId`, `workspaceId`,\n * `roleId`, `roleAssignmentId`, `userId`) and TR-024 denormalized\n * display names (`denormalizedUserName`, `denormalizedRoleName`).\n *\n * **Rename-cascade interaction (TR-023, Phase 6).** The SK uses the\n * raw `<roleId>` (rename-stable) for the discriminator and\n * `<normalizedUserName>` for the secondary sort. A Role rename does NOT\n * rewrite this SK; a User rename DOES (cascaded by the rename pipeline).\n *\n * **No GSI projection.** Per ADR-018 § Decision, cross-cutting reads\n * are served by the main-table partition\n * `TID#<tenantId>#WORKSPACE#ID#<workspaceId>`; the GSI1/GSI2 catalog\n * is unchanged.\n *\n * @see ADR-018 § Access Pattern Coverage (#9)\n * @see .state/adr-018-implementation-guide.md § 1 (SK grammar) and § 2 (attribute set)\n * @see .claude/rules/data-layer-layout.md — projection writers live in operations, not here\n */\nexport const RoleAssignmentWorkspaceProjectionEntity = new Entity({\n model: {\n entity: \"roleAssignmentWorkspaceProjection\",\n service: \"control\",\n version: \"01\",\n },\n attributes: {\n /**\n * Tenant the workspace belongs to. Renders as the leading segment\n * of the base-table PK. Always required — the workspace partition\n * is tenant-scoped per ADR-011.\n */\n tenantId: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Workspace partition discriminator. Renders as the trailing\n * segment of the base-table PK\n * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —\n * the projection has no meaning outside a workspace partition.\n */\n workspaceId: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Pre-composed sort key — built by the operations-layer projection\n * writer via `buildRoleAssignmentWorkspaceProjectionSk`. The entity\n * stores the value verbatim so the SK grammar (pattern #9) is\n * owned by the operations layer, not duplicated here.\n */\n sk: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * User the role assignment grants the role to. Stored as a\n * discriminating field so consumers can hydrate the canonical User\n * row via `UserEntity.get({ id: userId, sk: \"CURRENT\" })` when the\n * projection's `summary` is insufficient.\n */\n userId: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Role the assignment grants. Stored as a discriminating field —\n * also rendered into the SK as the discriminator-first segment so\n * `begins_with('ROLEASSIGNMENT#<roleId>#')` filters one role.\n */\n roleId: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * RoleAssignment canonical-record id. Stored as a discriminating\n * field so consumers can hydrate the canonical row via\n * `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`\n * when the projection's `summary` is insufficient.\n */\n roleAssignmentId: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Summary projection (key display fields as JSON string: id,\n * displayName, status) — mirrored from the canonical RoleAssignment\n * row so workspace-partition queries do not need a BatchGet hop.\n */\n summary: {\n type: \"string\" as const,\n required: true,\n },\n /** Version id mirrored from the canonical RoleAssignment row. */\n vid: {\n type: \"string\" as const,\n required: true,\n },\n /** Last-updated timestamp mirrored from the canonical RoleAssignment row. */\n lastUpdated: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Denormalized User display name — required to compose the\n * pattern-#9 SK (`ROLEASSIGNMENT#<roleId>#<normalizedUserName>#…`).\n * Optional on the schema because pre-TR-024 rows may not carry a\n * display name; the operations layer falls back to a sentinel when\n * missing so the SK still has a valid shape. The TR-023 rename-\n * cascade pipeline rewrites the SK on a User rename.\n */\n denormalizedUserName: {\n type: \"string\" as const,\n required: false,\n },\n /**\n * Denormalized Role display name — mirrored from the canonical\n * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).\n * Carried on the projection so consumers can render the role's\n * display name without a hop to the Role record. Not part of the\n * SK (pattern #9 sorts on `<normalizedUserName>`, not role name) —\n * a Role rename does NOT rewrite this SK.\n */\n denormalizedRoleName: {\n type: \"string\" as const,\n required: false,\n },\n },\n indexes: {\n /**\n * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,\n * SK = operation-supplied. Pattern #9 uses this index — the SK\n * encodes the entity-type prefix and discriminator-first roleId\n * (`ROLEASSIGNMENT#<roleId>#…`) so\n * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'ROLEASSIGNMENT#<roleId>#')`\n * returns every user-assignment for that role in the workspace, sorted\n * by normalized user name.\n */\n record: {\n pk: {\n field: \"PK\",\n composite: [\"tenantId\", \"workspaceId\"],\n template: \"TID#${tenantId}#WORKSPACE#ID#${workspaceId}\",\n },\n sk: {\n field: \"SK\",\n casing: \"none\" as const,\n composite: [\"sk\"],\n template: \"${sk}\",\n },\n },\n },\n});\n","import { Entity } from \"electrodb\";\nimport { gsi1ShardAttribute, gsi1skAttribute } from \"./control-entity-common\";\n\n/**\n * Tenant data-store entity (single-table store).\n *\n * **Classification (ADR 2026-03-03-01):** Tenant-isolated, control plane. Tenant IS the top scope;\n * the workspace dimension is not applicable and uses the sentinel `TENANT`. The tenant's own `id`\n * is stored as `tenantId` to drive the partition key.\n *\n * Key structure: PK = TENANT#ID#<tenantId>, SK = CURRENT.\n * Uniqueness: one Tenant per tenantId (id).\n *\n * GSI1 — Unified Sharded List per ADR-011: lists all Tenants across the four shards. Tenant has\n * no parent tenant or workspace, so the PK uses `TID#-#WID#-` sentinels.\n *\n * @see sites/www-docs/content/architecture/adr/2026-03-03-01-tenant-isolated-vs-non-tenant-isolated-entities.md\n * @see sites/www-docs/content/architecture/adr/2026-03-13-01-tenant-and-workspace-fhir-types-control-plane.md\n * @see sites/www-docs/content/packages/@openhi/constructs/data/dynamo/single-table-design.md\n */\nexport const TenantEntity = new Entity({\n model: {\n entity: \"tenant\",\n service: \"control\",\n version: \"01\",\n },\n attributes: {\n /** Sort key sentinel. Always \"CURRENT\". */\n sk: {\n type: \"string\" as const,\n required: true,\n default: \"CURRENT\",\n },\n /** The tenant's own id (= resource id). Drives the partition key. */\n tenantId: {\n type: \"string\" as const,\n required: true,\n },\n /** FHIR Resource.id; logical id in URL. Equals tenantId. */\n id: {\n type: \"string\" as const,\n required: true,\n },\n /** Full Tenant resource serialized as JSON string. */\n resource: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Summary projection (key display fields as JSON string: id, displayName, status).\n * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.\n */\n summary: {\n type: \"string\" as const,\n required: true,\n },\n /** Version id (e.g. ULID). */\n vid: {\n type: \"string\" as const,\n required: true,\n },\n lastUpdated: {\n type: \"string\" as const,\n required: true,\n },\n gsi1Shard: gsi1ShardAttribute,\n /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */\n gsi1sk: gsi1skAttribute,\n deleted: {\n type: \"boolean\" as const,\n required: false,\n },\n bundleId: {\n type: \"string\" as const,\n required: false,\n },\n msgId: {\n type: \"string\" as const,\n required: false,\n },\n },\n indexes: {\n /** Base table: PK = TENANT#ID#<tenantId>, SK = CURRENT. Do not supply PK or SK from outside. */\n record: {\n pk: {\n field: \"PK\",\n composite: [\"tenantId\"],\n template: \"TENANT#ID#${tenantId}\",\n },\n sk: {\n field: \"SK\",\n composite: [\"sk\"],\n template: \"${sk}\",\n },\n },\n\n /**\n * GSI1 — Unified Sharded List per ADR-011: list all Tenants across the four shards.\n * Tenant lives at the platform tier (no parent tenant or workspace), so `TID#-#WID#-`\n * sentinels precede `RT#Tenant#SHARD#<n>`. SK is derived via `gsi1skAttribute` —\n * `<normalizedName>#<id>` when the resource carries a `name`, else `<lastUpdated>#<id>`\n * (DR-004). `casing: \"none\"` preserves the normalized label and ISO-8601 `T`/`Z`.\n */\n gsi1: {\n index: \"GSI1\",\n pk: {\n field: \"GSI1PK\",\n composite: [\"gsi1Shard\"],\n template: \"TID#-#WID#-#RT#Tenant#SHARD#${gsi1Shard}\",\n },\n sk: {\n field: \"GSI1SK\",\n casing: \"none\" as const,\n composite: [\"gsi1sk\"],\n template: \"${gsi1sk}\",\n },\n },\n },\n});\n","import { Entity } from \"electrodb\";\nimport { gsi1ShardAttribute, gsi1skAttribute } from \"./control-entity-common\";\n\n/**\n * User data-store entity (single-table store).\n *\n * **Classification (ADR 2026-03-03-01):** Non-tenant-isolated, control plane. User is a\n * platform-wide identity; association with tenants and workspaces is through Membership and\n * RoleAssignment, not the User entity's own key.\n *\n * Key structure: PK = USER#ID#<id>, SK = CURRENT.\n * The USER# prefix prevents key collisions with other non-tenant-isolated entities (Role, etc.)\n * sharing the same table (ADR 2026-03-11-01 — preferred pattern for all control plane entities).\n * Uniqueness: one User per id.\n *\n * GSI1 — Unified Sharded List per ADR-011: lists all Users across the four shards. Non-tenant-\n * isolated, so the PK uses `TID#-#WID#-` sentinels.\n * GSI2 — Cognito sub-lookup per ADR-011: resolves a UserEntity from a Cognito `sub` claim\n * (`USER#SUB#<cognitoSub>` PK, `CURRENT` SK). The `cognitoSub` attribute is populated by the\n * Post Confirmation Lambda (Epic #765 / #770); kept optional here until that write path lands.\n *\n * @see sites/www-docs/content/architecture/adr/2026-03-03-01-tenant-isolated-vs-non-tenant-isolated-entities.md\n * @see sites/www-docs/content/architecture/adr/2026-03-11-01-user-type-definition-fhir-and-data-layer.md\n * @see sites/www-docs/content/packages/@openhi/constructs/data/dynamo/single-table-design.md\n */\nexport const UserEntity = new Entity({\n model: {\n entity: \"user\",\n service: \"control\",\n version: \"01\",\n },\n attributes: {\n /** Sort key sentinel. Always \"CURRENT\". */\n sk: {\n type: \"string\" as const,\n required: true,\n default: \"CURRENT\",\n },\n /** FHIR Resource.id; platform user id (ohi_uid). */\n id: {\n type: \"string\" as const,\n required: true,\n },\n /** Full User resource serialized as JSON string. */\n resource: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Summary projection (key display fields as JSON string: id, displayName, status).\n * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.\n */\n summary: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Immutable Cognito-issued `sub` claim. Drives GSI2 (sub-lookup). Optional until the\n * Post Confirmation Lambda (#770) lands; required thereafter.\n */\n cognitoSub: {\n type: \"string\" as const,\n required: false,\n },\n /** Version id (e.g. ULID). */\n vid: {\n type: \"string\" as const,\n required: true,\n },\n lastUpdated: {\n type: \"string\" as const,\n required: true,\n },\n gsi1Shard: gsi1ShardAttribute,\n /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */\n gsi1sk: gsi1skAttribute,\n deleted: {\n type: \"boolean\" as const,\n required: false,\n },\n /**\n * TR-022 / ADR-018 lifecycle state for the cascade pipeline.\n *\n * - `active` (or undefined) — normal, readable state.\n * - `deleting` — intermediate state set synchronously by the\n * hard-delete API entry point. The owning-delete cascade state\n * machine fans out from this transition (DynamoDB stream →\n * `control-plane.owning-delete.v1` → Step Functions). Readers MUST\n * short-circuit on `deleting` so partial cascades stay invisible.\n * - `deleted-failed` — terminal failure state set by the cascade\n * finalize Lambda when the cascade run fails irrecoverably.\n * Operators recover by re-running the cascade or by direct\n * intervention.\n *\n * The cascade finalize step deletes the canonical record conditional\n * on `lifecycleState = \"deleting\"`; on replay the conditional check\n * fails and the finalize step treats that as a no-op success.\n */\n lifecycleState: {\n type: [\"active\", \"deleting\", \"deleted-failed\"] as const,\n required: false,\n },\n bundleId: {\n type: \"string\" as const,\n required: false,\n },\n msgId: {\n type: \"string\" as const,\n required: false,\n },\n },\n indexes: {\n /** Base table: PK = USER#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */\n record: {\n pk: {\n field: \"PK\",\n composite: [\"id\"],\n template: \"USER#ID#${id}\",\n },\n sk: {\n field: \"SK\",\n composite: [\"sk\"],\n template: \"${sk}\",\n },\n },\n\n /**\n * GSI1 — Unified Sharded List per ADR-011: list all Users across the four shards.\n * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#User#SHARD#<n>`.\n * SK is derived via `gsi1skAttribute` — uses the resource's natural label when\n * extractable (string `name`/`title` via introspection), else `<lastUpdated>#<id>`\n * (DR-004). `casing: \"none\"` preserves the normalized label and ISO-8601 `T`/`Z`.\n */\n gsi1: {\n index: \"GSI1\",\n pk: {\n field: \"GSI1PK\",\n composite: [\"gsi1Shard\"],\n template: \"TID#-#WID#-#RT#User#SHARD#${gsi1Shard}\",\n },\n sk: {\n field: \"GSI1SK\",\n casing: \"none\" as const,\n composite: [\"gsi1sk\"],\n template: \"${gsi1sk}\",\n },\n },\n\n /**\n * GSI2 — Cognito sub-lookup per ADR-011: resolves the UserEntity from a Cognito `sub` claim.\n * `condition` skips the index when `cognitoSub` is missing so legacy items without a sub are\n * not indexed.\n */\n gsi2: {\n index: \"GSI2\",\n condition: (attrs: { cognitoSub?: string }) =>\n typeof attrs.cognitoSub === \"string\" && attrs.cognitoSub.length > 0,\n pk: {\n field: \"GSI2PK\",\n casing: \"none\" as const,\n composite: [\"cognitoSub\"],\n template: \"USER#SUB#${cognitoSub}\",\n },\n sk: {\n field: \"GSI2SK\",\n casing: \"none\" as const,\n composite: [],\n template: \"CURRENT\",\n },\n },\n },\n});\n","import { Entity } from \"electrodb\";\nimport { gsi1ShardAttribute, gsi1skAttribute } from \"./control-entity-common\";\n\n/**\n * Workspace data-store entity (single-table store).\n *\n * **Classification (ADR 2026-03-03-01):** Tenant-isolated, control plane. Each workspace belongs\n * to exactly one tenant; both tenantId and workspace id are in the partition key.\n *\n * Key structure: PK = TID#<tenantId>#WORKSPACE#ID#<id>, SK = CURRENT.\n * Uniqueness: one Workspace per (tenantId, id).\n *\n * GSI1 — Unified Sharded List per ADR-011: lists all Workspaces in a tenant across the four\n * shards. Workspace is itself the workspace identity, so the GSI1 PK uses `WID#-` as a sentinel.\n *\n * @see sites/www-docs/content/architecture/adr/2026-03-03-01-tenant-isolated-vs-non-tenant-isolated-entities.md\n * @see sites/www-docs/content/architecture/adr/2026-03-13-01-tenant-and-workspace-fhir-types-control-plane.md\n * @see sites/www-docs/content/packages/@openhi/constructs/data/dynamo/single-table-design.md\n */\nexport const WorkspaceEntity = new Entity({\n model: {\n entity: \"workspace\",\n service: \"control\",\n version: \"01\",\n },\n attributes: {\n /** Sort key sentinel. Always \"CURRENT\". */\n sk: {\n type: \"string\" as const,\n required: true,\n default: \"CURRENT\",\n },\n /** Tenant that contains this workspace (required). */\n tenantId: {\n type: \"string\" as const,\n required: true,\n },\n /** FHIR Resource.id; logical id in URL. */\n id: {\n type: \"string\" as const,\n required: true,\n },\n /** Full Workspace resource serialized as JSON string. */\n resource: {\n type: \"string\" as const,\n required: true,\n },\n /**\n * Summary projection (key display fields as JSON string: id, displayName, status).\n * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.\n */\n summary: {\n type: \"string\" as const,\n required: true,\n },\n /** Version id (e.g. ULID). */\n vid: {\n type: \"string\" as const,\n required: true,\n },\n lastUpdated: {\n type: \"string\" as const,\n required: true,\n },\n gsi1Shard: gsi1ShardAttribute,\n /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */\n gsi1sk: gsi1skAttribute,\n deleted: {\n type: \"boolean\" as const,\n required: false,\n },\n /**\n * TR-022 / ADR-018 lifecycle state for the cascade pipeline.\n *\n * - `active` (or undefined) — normal, readable state.\n * - `deleting` — intermediate state set synchronously by the\n * hard-delete API entry point. The owning-delete cascade state\n * machine fans out from this transition (DynamoDB stream →\n * `control-plane.owning-delete.v1` → Step Functions). Readers MUST\n * short-circuit on `deleting` so partial cascades stay invisible.\n * - `deleted-failed` — terminal failure state set by the cascade\n * finalize Lambda when the cascade run fails irrecoverably.\n * Operators recover by re-running the cascade or by direct\n * intervention.\n *\n * The cascade finalize step deletes the canonical record conditional\n * on `lifecycleState = \"deleting\"`; on replay the conditional check\n * fails and the finalize step treats that as a no-op success.\n */\n lifecycleState: {\n type: [\"active\", \"deleting\", \"deleted-failed\"] as const,\n required: false,\n },\n bundleId: {\n type: \"string\" as const,\n required: false,\n },\n msgId: {\n type: \"string\" as const,\n required: false,\n },\n },\n indexes: {\n /** Base table: PK = TID#<tenantId>#WORKSPACE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */\n record: {\n pk: {\n field: \"PK\",\n composite: [\"tenantId\", \"id\"],\n template: \"TID#${tenantId}#WORKSPACE#ID#${id}\",\n },\n sk: {\n field: \"SK\",\n composite: [\"sk\"],\n template: \"${sk}\",\n },\n },\n\n /**\n * GSI1 — Unified Sharded List per ADR-011: list all Workspaces for a tenant across the\n * four shards. Workspace is itself the workspace identity, so `WID#-` is a sentinel.\n * SK is derived via `gsi1skAttribute` — `<normalizedName>#<id>` when the resource\n * carries a `name`, else `<lastUpdated>#<id>` (DR-004). `casing: \"none\"` preserves\n * the normalized label and ISO-8601 `T`/`Z`.\n */\n gsi1: {\n index: \"GSI1\",\n pk: {\n field: \"GSI1PK\",\n composite: [\"tenantId\", \"gsi1Shard\"],\n template: \"TID#${tenantId}#WID#-#RT#Workspace#SHARD#${gsi1Shard}\",\n },\n sk: {\n field: \"GSI1SK\",\n casing: \"none\" as const,\n composite: [\"gsi1sk\"],\n template: \"${gsi1sk}\",\n },\n },\n },\n});\n"],"mappings":"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","names":["Entity","Entity","Entity","Entity","Entity","Entity","Entity","Entity","Entity","Entity","Entity","Entity","Entity","Entity","Entity","Entity","Entity","Entity"]}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
createPatientOperation,
|
|
6
6
|
createPractitionerOperation,
|
|
7
7
|
getRoleByIdOperation
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-E6MCKJVS.mjs";
|
|
9
9
|
import {
|
|
10
10
|
require_lib
|
|
11
11
|
} from "./chunk-ZM4GDHHC.mjs";
|
|
@@ -14,13 +14,13 @@ import {
|
|
|
14
14
|
createRoleAssignmentOperation,
|
|
15
15
|
createTenantOperation,
|
|
16
16
|
createWorkspaceOperation
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-AWYZJFPL.mjs";
|
|
18
18
|
import {
|
|
19
19
|
NotFoundError
|
|
20
20
|
} from "./chunk-FYHBHHWK.mjs";
|
|
21
21
|
import {
|
|
22
22
|
getDynamoControlService
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-VZCPGQXA.mjs";
|
|
24
24
|
import {
|
|
25
25
|
__toESM
|
|
26
26
|
} from "./chunk-LZOMFHX3.mjs";
|
|
@@ -932,4 +932,4 @@ export {
|
|
|
932
932
|
productionCognitoProvisioner,
|
|
933
933
|
handler
|
|
934
934
|
};
|
|
935
|
-
//# sourceMappingURL=chunk-
|
|
935
|
+
//# sourceMappingURL=chunk-WGA43MMY.mjs.map
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-QJDHVMKT.mjs";
|
|
5
5
|
import {
|
|
6
6
|
getDynamoControlService
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-VZCPGQXA.mjs";
|
|
8
8
|
|
|
9
9
|
// src/data/operations/control/rename-cascade/rename-cascade-rewrite-chunk-operation.ts
|
|
10
10
|
var RENAME_CASCADE_MAX_TARGETS_PER_CHUNK = 50;
|
|
@@ -63,4 +63,4 @@ export {
|
|
|
63
63
|
rewriteRenameCascadeChunkOperation,
|
|
64
64
|
chunkRenameCascadeTargets
|
|
65
65
|
};
|
|
66
|
-
//# sourceMappingURL=chunk-
|
|
66
|
+
//# sourceMappingURL=chunk-X5E4YJGZ.mjs.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getDynamoControlService
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-VZCPGQXA.mjs";
|
|
4
4
|
|
|
5
5
|
// src/data/operations/control/role/role-create-operation.ts
|
|
6
6
|
import { extractSummary } from "@openhi/types";
|
|
@@ -30,4 +30,4 @@ async function createRoleOperation(params) {
|
|
|
30
30
|
export {
|
|
31
31
|
createRoleOperation
|
|
32
32
|
};
|
|
33
|
-
//# sourceMappingURL=chunk-
|
|
33
|
+
//# sourceMappingURL=chunk-ZWSGM6PZ.mjs.map
|
|
@@ -4,7 +4,7 @@ import { C as CascadeChunkInput } from './events-CjS-sm0W.mjs';
|
|
|
4
4
|
* @see sites/www-docs/content/packages/@openhi/constructs/workflows/control-plane/owning-delete-cascade/delete-chunk-handler.md
|
|
5
5
|
*
|
|
6
6
|
* Cascade Step Functions Map-iteration handler. Receives one chunk of
|
|
7
|
-
*
|
|
7
|
+
* \<=100 projection rows from the list-and-chunk step and issues a
|
|
8
8
|
* single `TransactWriteItems` via `executeMultiWrite` (#1010).
|
|
9
9
|
*
|
|
10
10
|
* Idempotency: ElectroDB / DynamoDB SDK forwards the `chunkToken` as
|
|
@@ -4,7 +4,7 @@ import { C as CascadeChunkInput } from './events-CjS-sm0W.js';
|
|
|
4
4
|
* @see sites/www-docs/content/packages/@openhi/constructs/workflows/control-plane/owning-delete-cascade/delete-chunk-handler.md
|
|
5
5
|
*
|
|
6
6
|
* Cascade Step Functions Map-iteration handler. Receives one chunk of
|
|
7
|
-
*
|
|
7
|
+
* \<=100 projection rows from the list-and-chunk step and issues a
|
|
8
8
|
* single `TransactWriteItems` via `executeMultiWrite` (#1010).
|
|
9
9
|
*
|
|
10
10
|
* Idempotency: ElectroDB / DynamoDB SDK forwards the `chunkToken` as
|