import {
  defaultTableName,
  dynamoClient
} from "./chunk-TRY7JGWO.mjs";

// src/data/dynamo/dynamo-control-service.ts
import { Service } from "electrodb";

// src/data/dynamo/entities/control/configuration-entity.ts
import { Entity } from "electrodb";

// src/data/dynamo/entities/control/control-entity-common.ts
import { extractLabel, normalizeLabel } from "@openhi/types";

// src/data/dynamo/shard.ts
var SHARD_COUNT = 4;
function computeShard(id) {
  let hash = 2166136261;
  for (let i = 0; i < id.length; i++) {
    hash ^= id.charCodeAt(i);
    hash = Math.imul(hash, 16777619);
  }
  return (hash >>> 0) % SHARD_COUNT;
}

// src/data/dynamo/entities/control/control-entity-common.ts
var gsi1ShardAttribute = {
  type: "string",
  watch: ["id"],
  set: (_val, item) => {
    if (typeof item?.id !== "string" || item.id.length === 0) {
      return void 0;
    }
    return String(computeShard(item.id));
  }
};
var gsi1skAttribute = {
  type: "string",
  watch: ["resource", "lastUpdated", "id"],
  set: (_val, item) => {
    const id = typeof item?.id === "string" ? item.id : "";
    const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
    const fallback = `${lastUpdated}#${id}`;
    if (typeof item?.resource !== "string" || item.resource.length === 0) {
      return fallback;
    }
    let parsed;
    try {
      parsed = JSON.parse(item.resource);
    } catch {
      return fallback;
    }
    if (!parsed || typeof parsed !== "object") return fallback;
    const resourceType = parsed.resourceType;
    if (typeof resourceType !== "string") return fallback;
    const label = extractLabel(parsed);
    return label !== void 0 ? `${label}#${id}` : fallback;
  }
};
function extractRoleId(resource) {
  const flat = resource.roleId;
  if (typeof flat === "string" && flat.length > 0) return flat;
  const role = resource.role;
  if (role && typeof role === "object") {
    const reference = role.reference;
    if (typeof reference === "string" && reference.length > 0) {
      const slash = reference.lastIndexOf("/");
      const tail = slash >= 0 ? reference.slice(slash + 1) : reference;
      if (tail.length > 0) return tail;
    }
  }
  return void 0;
}
var roleAssignmentGsi1skAttribute = {
  type: "string",
  watch: ["resource", "denormalizedUserName", "lastUpdated", "id"],
  set: (_val, item) => {
    const id = typeof item?.id === "string" ? item.id : "";
    const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
    const fallback = `${lastUpdated}#${id}`;
    if (typeof item?.resource !== "string" || item.resource.length === 0) {
      return fallback;
    }
    let parsed;
    try {
      parsed = JSON.parse(item.resource);
    } catch {
      return fallback;
    }
    if (!parsed || typeof parsed !== "object") return fallback;
    const roleId = extractRoleId(parsed);
    if (roleId === void 0) return fallback;
    const denormalizedUserName = typeof item.denormalizedUserName === "string" ? item.denormalizedUserName : "";
    const normalizedUserName = denormalizedUserName.length > 0 ? normalizeLabel(denormalizedUserName) : "";
    if (normalizedUserName.length === 0) return fallback;
    return `${roleId}#${normalizedUserName}#${id}`;
  }
};
var membershipGsi1skAttribute = {
  type: "string",
  watch: ["denormalizedUserName", "lastUpdated", "id"],
  set: (_val, item) => {
    const id = typeof item?.id === "string" ? item.id : "";
    const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
    const fallback = `${lastUpdated}#${id}`;
    const denormalizedUserName = typeof item?.denormalizedUserName === "string" ? item.denormalizedUserName : "";
    const normalizedUserName = denormalizedUserName.length > 0 ? normalizeLabel(denormalizedUserName) : "";
    if (normalizedUserName.length === 0) {
      return fallback;
    }
    return `${normalizedUserName}#${id}`;
  }
};

// src/data/dynamo/entities/control/configuration-entity.ts
var ConfigurationEntity = new Entity({
  model: {
    entity: "configuration",
    service: "control",
    version: "01"
  },
  attributes: {
    /** Sort key. "CURRENT" for current version; version history in S3. */
    sk: {
      type: "string",
      required: true,
      default: "CURRENT"
    },
    /** Tenant scope. Use "BASELINE" when the config is baseline default (no tenant). */
    tenantId: {
      type: "string",
      required: true,
      default: "BASELINE"
    },
    /** Workspace scope. Use "-" when absent. */
    workspaceId: {
      type: "string",
      required: true,
      default: "-"
    },
    /** User scope. Use "-" when absent. */
    userId: {
      type: "string",
      required: true,
      default: "-"
    },
    /** Role scope. Use "-" when absent. */
    roleId: {
      type: "string",
      required: true,
      default: "-"
    },
    /** Config type (category), e.g. endpoints, branding, display. */
    key: {
      type: "string",
      required: true
    },
    /** FHIR Resource.id; logical id in URL and for the Configuration resource. */
    id: {
      type: "string",
      required: true
    },
    /** Payload as JSON string. JSON.stringify(resource) on write; JSON.parse(item.resource) on read. */
    resource: {
      type: "string",
      required: true
    },
    /**
     * Summary projection (key display fields as JSON string: id, key, status).
     * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
     */
    summary: {
      type: "string",
      required: true
    },
    /** Version id (e.g. ULID). Tracks current version; S3 history key. */
    vid: {
      type: "string",
      required: true
    },
    lastUpdated: {
      type: "string",
      required: true
    },
    gsi1Shard: gsi1ShardAttribute,
    deleted: {
      type: "boolean",
      required: false
    },
    bundleId: {
      type: "string",
      required: false
    },
    msgId: {
      type: "string",
      required: false
    }
  },
  indexes: {
    /** 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. */
    record: {
      pk: {
        field: "PK",
        composite: ["tenantId", "workspaceId", "userId", "roleId"],
        template: "CONFIG#TID#${tenantId}#WID#${workspaceId}#UID#${userId}#RID#${roleId}"
      },
      sk: {
        field: "SK",
        composite: ["key", "sk"],
        template: "KEY#${key}#SK#${sk}"
      }
    },
    /**
     * GSI1 — Unified Sharded List per ADR-011: list all Configuration entries for a
     * (tenant, workspace) across the four shards. Use for "list configs scoped to this tenant"
     * (workspaceId = "-") or "list configs scoped to this workspace". Does not support
     * hierarchical resolution in one query; use base table GetItem in fallback order
     * (user → workspace → tenant → baseline) for that.
     * SK is `<key>#<id>` — Configuration's `key` is a required entity attribute (the
     * config category: endpoints, branding, display, …) and the natural sort/lookup
     * dimension. `casing: "none"` preserves the literal key value.
     */
    gsi1: {
      index: "GSI1",
      pk: {
        field: "GSI1PK",
        composite: ["tenantId", "workspaceId", "gsi1Shard"],
        template: "TID#${tenantId}#WID#${workspaceId}#RT#Configuration#SHARD#${gsi1Shard}"
      },
      sk: {
        field: "GSI1SK",
        casing: "none",
        composite: ["key", "id"],
        template: "${key}#${id}"
      }
    }
  }
});

// src/data/dynamo/entities/control/configuration-user-projection-entity.ts
import { Entity as Entity2 } from "electrodb";
var ConfigurationUserProjectionEntity = new Entity2({
  model: {
    entity: "configurationUserProjection",
    service: "control",
    version: "01"
  },
  attributes: {
    /**
     * User partition discriminator. Renders as `USER#ID#<userId>` on the
     * base-table PK. Always required — the projection has no meaning
     * outside a user partition.
     */
    userId: {
      type: "string",
      required: true
    },
    /**
     * Pre-composed sort key — built by the operations-layer projection
     * writer via `buildConfigurationUserProjectionSk`. The entity stores
     * the value verbatim so the SK grammar (pattern #10 user-scope) is
     * owned by the operations layer, not duplicated here.
     */
    sk: {
      type: "string",
      required: true
    },
    /**
     * Configuration canonical-record id. Stored as a discriminating
     * field so consumers can hydrate the canonical row via the
     * Configuration get-by-id operation when the projection's `summary`
     * is insufficient.
     */
    configurationId: {
      type: "string",
      required: true
    },
    /**
     * Tenant the Configuration is associated with. The canonical row
     * keys off `(tenantId, workspaceId, userId, roleId)`; the projection
     * carries `tenantId` so consumers reconstructing the canonical PK
     * have the tenant segment without a hop.
     */
    tenantId: {
      type: "string",
      required: true
    },
    /**
     * Scope marker. Always `"user"` on this projection — recorded
     * explicitly so future scope-bearing projections (workspace,
     * tenant, role) can share filter semantics in a unified
     * cross-projection list query if one ever lands.
     */
    scope: {
      type: "string",
      required: true,
      default: "user"
    },
    /**
     * Configuration's `key` attribute (config category, e.g. endpoints,
     * branding, display). Mirrored from the canonical row so consumers
     * reading the projection get the natural display label without a
     * BatchGet hop. Doubles as the source of `<normalizedConfigName>` in
     * the SK.
     */
    displayName: {
      type: "string",
      required: false
    },
    /**
     * Summary projection (key display fields as JSON string) — mirrored
     * from the canonical Configuration row so user-partition queries do
     * not need a BatchGet hop.
     */
    summary: {
      type: "string",
      required: true
    },
    /** Version id mirrored from the canonical Configuration row. */
    vid: {
      type: "string",
      required: true
    },
    /** Last-updated timestamp mirrored from the canonical Configuration row. */
    lastUpdated: {
      type: "string",
      required: true
    }
  },
  indexes: {
    /**
     * Base table: PK = USER#ID#\<userId\>, SK = operation-supplied. A
     * single `Query(PK = USER#ID#<userId>, SK begins_with 'CONFIGURATION#')`
     * returns the user's user-scoped Configurations sorted by
     * `<normalizedConfigName>` (then `<configurationId>` as the
     * tiebreaker).
     */
    record: {
      pk: {
        field: "PK",
        composite: ["userId"],
        template: "USER#ID#${userId}"
      },
      sk: {
        field: "SK",
        casing: "none",
        composite: ["sk"],
        template: "${sk}"
      }
    }
  }
});

// src/data/dynamo/entities/control/configuration-workspace-projection-entity.ts
import { Entity as Entity3 } from "electrodb";
var ConfigurationWorkspaceProjectionEntity = new Entity3({
  model: {
    entity: "configurationWorkspaceProjection",
    service: "control",
    version: "01"
  },
  attributes: {
    /**
     * Tenant the workspace belongs to. Renders as the leading segment
     * of the base-table PK. Always required — the workspace partition
     * is tenant-scoped per ADR-011.
     */
    tenantId: {
      type: "string",
      required: true
    },
    /**
     * Workspace partition discriminator. Renders as the trailing
     * segment of the base-table PK
     * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
     * the projection has no meaning outside a workspace partition.
     */
    workspaceId: {
      type: "string",
      required: true
    },
    /**
     * Pre-composed sort key — built by the operations-layer projection
     * writer via `buildConfigurationWorkspaceProjectionSk`. The entity
     * stores the value verbatim so the SK grammar (pattern #10
     * workspace-scope) is owned by the operations layer, not
     * duplicated here.
     */
    sk: {
      type: "string",
      required: true
    },
    /**
     * Configuration canonical-record id. Stored as a discriminating
     * field so consumers can hydrate the canonical row via the
     * Configuration get-by-id operation when the projection's `summary`
     * is insufficient.
     */
    configurationId: {
      type: "string",
      required: true
    },
    /**
     * Scope marker. Always `"workspace"` on this projection — recorded
     * explicitly so future scope-bearing projections (user, tenant,
     * role) can share filter semantics in a unified cross-projection
     * list query if one ever lands.
     */
    scope: {
      type: "string",
      required: true,
      default: "workspace"
    },
    /**
     * Configuration's `key` attribute (config category, e.g. endpoints,
     * branding, display). Mirrored from the canonical row so consumers
     * reading the projection get the natural display label without a
     * BatchGet hop. Doubles as the source of `<normalizedConfigName>`
     * in the SK.
     */
    displayName: {
      type: "string",
      required: false
    },
    /**
     * Summary projection (key display fields as JSON string) — mirrored
     * from the canonical Configuration row so workspace-partition
     * queries do not need a BatchGet hop.
     */
    summary: {
      type: "string",
      required: true
    },
    /** Version id mirrored from the canonical Configuration row. */
    vid: {
      type: "string",
      required: true
    },
    /** Last-updated timestamp mirrored from the canonical Configuration row. */
    lastUpdated: {
      type: "string",
      required: true
    }
  },
  indexes: {
    /**
     * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
     * SK = operation-supplied. A single
     * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'CONFIGURATION#')`
     * returns the workspace's workspace-scoped Configurations sorted by
     * `<normalizedConfigName>` (then `<configurationId>` as the
     * tiebreaker).
     */
    record: {
      pk: {
        field: "PK",
        composite: ["tenantId", "workspaceId"],
        template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
      },
      sk: {
        field: "SK",
        casing: "none",
        composite: ["sk"],
        template: "${sk}"
      }
    }
  }
});

// src/data/dynamo/entities/control/membership-entity.ts
import { Entity as Entity4 } from "electrodb";
var MembershipEntity = new Entity4({
  model: {
    entity: "membership",
    service: "control",
    version: "01"
  },
  attributes: {
    /** Sort key sentinel. Always "CURRENT". */
    sk: {
      type: "string",
      required: true,
      default: "CURRENT"
    },
    /** Tenant in which the user has membership (required). */
    tenantId: {
      type: "string",
      required: true
    },
    /** FHIR Resource.id; membership id. */
    id: {
      type: "string",
      required: true
    },
    /** Full Membership resource serialized as JSON string. */
    resource: {
      type: "string",
      required: true
    },
    /**
     * Summary projection (key display fields as JSON string: id, displayName, status).
     * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
     */
    summary: {
      type: "string",
      required: true
    },
    /** Version id (e.g. ULID). */
    vid: {
      type: "string",
      required: true
    },
    lastUpdated: {
      type: "string",
      required: true
    },
    gsi1Shard: gsi1ShardAttribute,
    /**
     * Derived GSI1 sort key — `<normalizedUserName>#<id>` per ADR-018
     * pattern #1 so a GSI1 query partitioned on the tenant range-scans
     * by user-name prefix and returns memberships sorted by user name.
     * Falls back to `<lastUpdated>#<id>` when `denormalizedUserName`
     * is missing.
     */
    gsi1sk: membershipGsi1skAttribute,
    deleted: {
      type: "boolean",
      required: false
    },
    bundleId: {
      type: "string",
      required: false
    },
    msgId: {
      type: "string",
      required: false
    },
    /**
     * Denormalized `linked-data-identity` Reference (e.g. `Practitioner/abc`).
     * Populated from the FHIR extension on the Membership resource at write
     * time so future GSIs can index data-plane identity lookups without
     * deserializing the full resource JSON. See ADR 2026-03-13-02 §6.
     */
    linkedDataIdentityRef: {
      type: "string",
      required: false
    },
    /**
     * Denormalized display name of the linked Tenant, captured at row
     * last-write time. Promoted to a top-level attribute so the ADR-018
     * adjacency-list projection SKs (pattern #3 — `MEMBERSHIP#TENANT#<normalizedTenantName>#…`)
     * can be composed from a top-level field instead of digging into the
     * `resource` JSON. Optional on the schema so pre-TR-024 rows do not
     * break; the operations-layer multi-write helper (#1010) makes the
     * field load-bearing at write time per TR-024 rule 2 (write-time
     * source = canonical Tenant.displayName).
     * @see TR-024 — Denormalized display-name attributes
     */
    denormalizedTenantName: {
      type: "string",
      required: false
    },
    /**
     * Denormalized display name of the linked User, captured at row
     * last-write time. Promoted to a top-level attribute so the ADR-018
     * adjacency-list canonical-record GSI1SK (pattern #1 —
     * `<normalizedUserName>#<id>`) and workspace-projection SK (pattern #2)
     * can be composed from a top-level field. Optional on the schema so
     * pre-TR-024 rows do not break; the operations-layer multi-write helper
     * (#1010) makes the field load-bearing at write time per TR-024 rule 2
     * (write-time source = canonical User.displayName).
     * @see TR-024 — Denormalized display-name attributes
     */
    denormalizedUserName: {
      type: "string",
      required: false
    }
  },
  indexes: {
    /** Base table: PK = TID#<tenantId>#MEMBERSHIP#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
    record: {
      pk: {
        field: "PK",
        composite: ["tenantId", "id"],
        template: "TID#${tenantId}#MEMBERSHIP#ID#${id}"
      },
      sk: {
        field: "SK",
        composite: ["sk"],
        template: "${sk}"
      }
    },
    /**
     * GSI1 — Unified Sharded List per ADR-011: list all Memberships for a tenant across the
     * four shards. Membership is tenant-scoped only, so `WID#-` is a sentinel.
     * SK is derived via `membershipGsi1skAttribute` — composes
     * `<normalizedUserName>#<id>` per ADR-018 pattern #1 (users in a
     * tenant, sorted by user name); falls back to `<lastUpdated>#<id>`
     * when `denormalizedUserName` is missing. `casing: "none"` preserves
     * the normalized label and ISO-8601 `T`/`Z`.
     */
    gsi1: {
      index: "GSI1",
      pk: {
        field: "GSI1PK",
        composite: ["tenantId", "gsi1Shard"],
        template: "TID#${tenantId}#WID#-#RT#Membership#SHARD#${gsi1Shard}"
      },
      sk: {
        field: "GSI1SK",
        casing: "none",
        composite: ["gsi1sk"],
        template: "${gsi1sk}"
      }
    }
  }
});

// src/data/dynamo/entities/control/membership-user-projection-entity.ts
import { Entity as Entity5 } from "electrodb";
var MembershipUserProjectionEntity = new Entity5({
  model: {
    entity: "membershipUserProjection",
    service: "control",
    version: "01"
  },
  attributes: {
    /**
     * User partition discriminator. Renders as `USER#ID#<userId>` on the
     * base-table PK. Always required — the projection has no meaning
     * outside a user partition.
     */
    userId: {
      type: "string",
      required: true
    },
    /**
     * Pre-composed sort key — built by the operations-layer projection
     * writer via `buildMembershipUserProjectionSk*` helpers. The entity
     * stores the value verbatim so the SK grammar (patterns #3 and #4)
     * is owned by the operations layer, not duplicated here.
     */
    sk: {
      type: "string",
      required: true
    },
    /** Tenant in which the membership applies. Always required. */
    tenantId: {
      type: "string",
      required: true
    },
    /**
     * Workspace the membership scopes to. Present iff the projection
     * row is a pattern-#4 workspace sub-lane row; absent for pattern-#3
     * tenant sub-lane rows.
     */
    workspaceId: {
      type: "string",
      required: false
    },
    /**
     * Membership canonical-record id. Stored as a discriminating field
     * so consumers can hydrate the canonical row via
     * `MembershipEntity.get({ tenantId, id: membershipId })` when the
     * projection's `summary` is insufficient.
     */
    membershipId: {
      type: "string",
      required: true
    },
    /**
     * Summary projection (key display fields as JSON string: id,
     * displayName, status) — mirrored from the canonical Membership row
     * so user-partition queries do not need a BatchGet hop.
     */
    summary: {
      type: "string",
      required: true
    },
    /** Version id mirrored from the canonical Membership row. */
    vid: {
      type: "string",
      required: true
    },
    /** Last-updated timestamp mirrored from the canonical Membership row. */
    lastUpdated: {
      type: "string",
      required: true
    },
    /**
     * Denormalized Tenant display name — required to compose pattern-#3
     * SK (`MEMBERSHIP#TENANT#<normalizedTenantName>#…`). Optional on the
     * schema because pre-TR-024 rows may not carry a display name; the
     * operations layer falls back gracefully when missing.
     */
    denormalizedTenantName: {
      type: "string",
      required: false
    },
    /**
     * Denormalized User display name — mirrored from the canonical
     * Membership row per TR-024 rule 3 (canonical-record symmetry).
     * Carried on the projection so consumers can render the user's
     * display name without a hop to the User record.
     */
    denormalizedUserName: {
      type: "string",
      required: false
    },
    /**
     * Denormalized Workspace display name — required to compose
     * pattern-#4 SK (`MEMBERSHIP#WORKSPACE#TID#<tenantId>#<normalizedWorkspaceName>#…`).
     * Optional on the schema (TR-024 § Open Item #4 defers a formal
     * Workspace-rename cascade); the operations layer falls back to a
     * sentinel when missing so the SK still has a valid shape.
     */
    denormalizedWorkspaceName: {
      type: "string",
      required: false
    }
  },
  indexes: {
    /**
     * Base table: PK = USER#ID#\<userId\>, SK = operation-supplied.
     * Both pattern #3 and pattern #4 use this same index — the SK string
     * encodes the lane discriminator (`MEMBERSHIP#TENANT#…` vs
     * `MEMBERSHIP#WORKSPACE#…`) so a single
     * `Query(PK = USER#ID#<userId>, SK begins_with 'MEMBERSHIP#')`
     * returns both lanes interleaved.
     */
    record: {
      pk: {
        field: "PK",
        composite: ["userId"],
        template: "USER#ID#${userId}"
      },
      sk: {
        field: "SK",
        casing: "none",
        composite: ["sk"],
        template: "${sk}"
      }
    }
  }
});

// src/data/dynamo/entities/control/membership-workspace-projection-entity.ts
import { Entity as Entity6 } from "electrodb";
var MembershipWorkspaceProjectionEntity = new Entity6({
  model: {
    entity: "membershipWorkspaceProjection",
    service: "control",
    version: "01"
  },
  attributes: {
    /**
     * Tenant the workspace belongs to. Renders as the leading segment
     * of the base-table PK. Always required — the workspace partition
     * is tenant-scoped per ADR-011.
     */
    tenantId: {
      type: "string",
      required: true
    },
    /**
     * Workspace partition discriminator. Renders as the trailing
     * segment of the base-table PK
     * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
     * the projection has no meaning outside a workspace partition.
     */
    workspaceId: {
      type: "string",
      required: true
    },
    /**
     * Pre-composed sort key — built by the operations-layer projection
     * writer via `buildMembershipWorkspaceProjectionSk`. The entity
     * stores the value verbatim so the SK grammar (pattern #2) is
     * owned by the operations layer, not duplicated here.
     */
    sk: {
      type: "string",
      required: true
    },
    /**
     * User the membership links. Stored as a discriminating field so
     * consumers can hydrate the canonical User row via
     * `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
     * projection's `summary` is insufficient.
     */
    userId: {
      type: "string",
      required: true
    },
    /**
     * Membership canonical-record id. Stored as a discriminating field
     * so consumers can hydrate the canonical row via
     * `MembershipEntity.get({ tenantId, id: membershipId })` when the
     * projection's `summary` is insufficient.
     */
    membershipId: {
      type: "string",
      required: true
    },
    /**
     * Summary projection (key display fields as JSON string: id,
     * displayName, status) — mirrored from the canonical Membership row
     * so workspace-partition queries do not need a BatchGet hop.
     */
    summary: {
      type: "string",
      required: true
    },
    /** Version id mirrored from the canonical Membership row. */
    vid: {
      type: "string",
      required: true
    },
    /** Last-updated timestamp mirrored from the canonical Membership row. */
    lastUpdated: {
      type: "string",
      required: true
    },
    /**
     * Denormalized User display name — required to compose the
     * pattern-#2 SK (`MEMBERSHIP#<normalizedUserName>#…`). Optional on
     * the schema because pre-TR-024 rows may not carry a display name;
     * the operations layer falls back to a sentinel when missing so
     * the SK still has a valid shape. The TR-023 rename-cascade
     * pipeline rewrites the SK on a User rename.
     */
    denormalizedUserName: {
      type: "string",
      required: false
    }
  },
  indexes: {
    /**
     * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
     * SK = operation-supplied. Pattern #2 uses this index — the SK
     * encodes the entity-type prefix (`MEMBERSHIP#…`) so a
     * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'MEMBERSHIP#')`
     * returns every member projection for the workspace in normalized-
     * user-name sort order.
     */
    record: {
      pk: {
        field: "PK",
        composite: ["tenantId", "workspaceId"],
        template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
      },
      sk: {
        field: "SK",
        casing: "none",
        composite: ["sk"],
        template: "${sk}"
      }
    }
  }
});

// src/data/dynamo/entities/control/role-entity.ts
import { Entity as Entity7 } from "electrodb";
var RoleEntity = new Entity7({
  model: {
    entity: "role",
    service: "control",
    version: "01"
  },
  attributes: {
    /** Sort key sentinel. Always "CURRENT". */
    sk: {
      type: "string",
      required: true,
      default: "CURRENT"
    },
    /** FHIR Resource.id; role id. */
    id: {
      type: "string",
      required: true
    },
    /** Full Role resource serialized as JSON string. */
    resource: {
      type: "string",
      required: true
    },
    /**
     * Summary projection (key display fields as JSON string: id, displayName, status).
     * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
     */
    summary: {
      type: "string",
      required: true
    },
    /** Version id (e.g. ULID). */
    vid: {
      type: "string",
      required: true
    },
    lastUpdated: {
      type: "string",
      required: true
    },
    gsi1Shard: gsi1ShardAttribute,
    /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
    gsi1sk: gsi1skAttribute,
    deleted: {
      type: "boolean",
      required: false
    },
    bundleId: {
      type: "string",
      required: false
    },
    msgId: {
      type: "string",
      required: false
    }
  },
  indexes: {
    /** Base table: PK = ROLE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
    record: {
      pk: {
        field: "PK",
        composite: ["id"],
        template: "ROLE#ID#${id}"
      },
      sk: {
        field: "SK",
        composite: ["sk"],
        template: "${sk}"
      }
    },
    /**
     * GSI1 — Unified Sharded List per ADR-011: list all Roles across the four shards.
     * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#Role#SHARD#<n>`.
     * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
     * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
     * normalized label and ISO-8601 `T`/`Z`.
     */
    gsi1: {
      index: "GSI1",
      pk: {
        field: "GSI1PK",
        composite: ["gsi1Shard"],
        template: "TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}"
      },
      sk: {
        field: "GSI1SK",
        casing: "none",
        composite: ["gsi1sk"],
        template: "${gsi1sk}"
      }
    }
  }
});

// src/data/dynamo/entities/control/roleassignment-entity.ts
import { Entity as Entity8 } from "electrodb";
var RoleAssignmentEntity = new Entity8({
  model: {
    entity: "roleassignment",
    service: "control",
    version: "01"
  },
  attributes: {
    /** Sort key sentinel. Always "CURRENT". */
    sk: {
      type: "string",
      required: true,
      default: "CURRENT"
    },
    /** Tenant in which the role assignment applies (required). */
    tenantId: {
      type: "string",
      required: true
    },
    /** FHIR Resource.id; role assignment id. */
    id: {
      type: "string",
      required: true
    },
    /** Full RoleAssignment resource serialized as JSON string. */
    resource: {
      type: "string",
      required: true
    },
    /**
     * Summary projection (key display fields as JSON string: id, displayName, status).
     * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
     */
    summary: {
      type: "string",
      required: true
    },
    /** Version id (e.g. ULID). */
    vid: {
      type: "string",
      required: true
    },
    lastUpdated: {
      type: "string",
      required: true
    },
    gsi1Shard: gsi1ShardAttribute,
    /**
     * Derived GSI1 sort key — discriminator-first
     * `<roleId>#<normalizedUserName>#<id>` per ADR-018 pattern #8 so a
     * GSI1 query partitioned on the tenant can `begins_with('<roleId>#')`
     * to enumerate every user assigned to a given role, sorted by user
     * name. Falls back to `<lastUpdated>#<id>` when either component is
     * missing.
     */
    gsi1sk: roleAssignmentGsi1skAttribute,
    deleted: {
      type: "boolean",
      required: false
    },
    bundleId: {
      type: "string",
      required: false
    },
    msgId: {
      type: "string",
      required: false
    },
    /**
     * Denormalized display name of the linked Tenant, captured at row
     * last-write time. Promoted to a top-level attribute so the ADR-018
     * adjacency-list user-projection SK (pattern #5 —
     * `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#<roleId>#TID#<tenantId>#<id>`)
     * can be composed from a top-level field instead of digging into the
     * `resource` JSON. Optional on the schema so pre-TR-024 rows do not
     * break; the operations-layer multi-write helper (#1010) makes the
     * field load-bearing at write time per TR-024 rule 2 (write-time
     * source = canonical Tenant.displayName).
     * @see TR-024 — Denormalized display-name attributes
     */
    denormalizedTenantName: {
      type: "string",
      required: false
    },
    /**
     * Denormalized display name of the linked User, captured at row
     * last-write time. Promoted to a top-level attribute so the ADR-018
     * adjacency-list canonical-record GSI1SK (pattern #8 —
     * `<roleId>#<normalizedUserName>#<id>`) and workspace-projection SK
     * (pattern #9) can be composed from a top-level field. Optional on
     * the schema so pre-TR-024 rows do not break; the operations-layer
     * multi-write helper (#1010) makes the field load-bearing at write
     * time per TR-024 rule 2 (write-time source = canonical
     * User.displayName).
     * @see TR-024 — Denormalized display-name attributes
     */
    denormalizedUserName: {
      type: "string",
      required: false
    },
    /**
     * Denormalized display name of the linked Role, captured at row
     * last-write time. Promoted to a top-level attribute so the ADR-018
     * adjacency-list user-projection SK (pattern #5 —
     * `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#…`) can be composed from
     * a top-level field. Optional on the schema so pre-TR-024 rows do not
     * break; the operations-layer multi-write helper (#1010) makes the
     * field load-bearing at write time per TR-024 rule 2 (write-time
     * source = canonical Role.displayName).
     * @see TR-024 — Denormalized display-name attributes
     */
    denormalizedRoleName: {
      type: "string",
      required: false
    }
  },
  indexes: {
    /** Base table: PK = TID#<tenantId>#ROLEASSIGNMENT#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
    record: {
      pk: {
        field: "PK",
        composite: ["tenantId", "id"],
        template: "TID#${tenantId}#ROLEASSIGNMENT#ID#${id}"
      },
      sk: {
        field: "SK",
        composite: ["sk"],
        template: "${sk}"
      }
    },
    /**
     * GSI1 — Unified Sharded List per ADR-011: list all RoleAssignments for a tenant across the
     * four shards. Tenant-scoped only, so `WID#-` is a sentinel.
     * SK is derived via `roleAssignmentGsi1skAttribute` — composes the
     * discriminator-first `<roleId>#<normalizedUserName>#<id>` shape per
     * ADR-018 pattern #8 (users with a specific role in a tenant, sorted
     * by user name); falls back to `<lastUpdated>#<id>` when either
     * component is missing. `casing: "none"` preserves the normalized
     * label and ISO-8601 `T`/`Z`.
     */
    gsi1: {
      index: "GSI1",
      pk: {
        field: "GSI1PK",
        composite: ["tenantId", "gsi1Shard"],
        template: "TID#${tenantId}#WID#-#RT#RoleAssignment#SHARD#${gsi1Shard}"
      },
      sk: {
        field: "GSI1SK",
        casing: "none",
        composite: ["gsi1sk"],
        template: "${gsi1sk}"
      }
    }
  }
});

// src/data/dynamo/entities/control/roleassignment-user-projection-entity.ts
import { Entity as Entity9 } from "electrodb";
var RoleAssignmentUserProjectionEntity = new Entity9({
  model: {
    entity: "roleAssignmentUserProjection",
    service: "control",
    version: "01"
  },
  attributes: {
    /**
     * User partition discriminator. Renders as `USER#ID#<userId>` on the
     * base-table PK. Always required — the projection has no meaning
     * outside a user partition.
     */
    userId: {
      type: "string",
      required: true
    },
    /**
     * Pre-composed sort key — built by the operations-layer projection
     * writer via `buildRoleAssignmentUserProjectionSk*` helpers. The
     * entity stores the value verbatim so the SK grammar (tenant-lane
     * vs workspace-lane) is owned by the operations layer, not
     * duplicated here.
     */
    sk: {
      type: "string",
      required: true
    },
    /** Tenant in which the role assignment applies. Always required. */
    tenantId: {
      type: "string",
      required: true
    },
    /**
     * Workspace the role assignment scopes to. Present iff the
     * projection row is the workspace-level sub-lane; absent for
     * tenant-level sub-lane rows.
     */
    workspaceId: {
      type: "string",
      required: false
    },
    /**
     * Role the assignment grants. Stored as a discriminating field so
     * `Query(PK = USER#ID#<userId>, SK begins_with 'ROLEASSIGNMENT#…')`
     * results carry the role id without a hop to the canonical row.
     */
    roleId: {
      type: "string",
      required: true
    },
    /**
     * RoleAssignment canonical-record id. Stored as a discriminating
     * field so consumers can hydrate the canonical row via
     * `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
     * when the projection's `summary` is insufficient.
     */
    roleAssignmentId: {
      type: "string",
      required: true
    },
    /**
     * Summary projection (key display fields as JSON string: id,
     * displayName, status) — mirrored from the canonical RoleAssignment
     * row so user-partition queries do not need a BatchGet hop.
     */
    summary: {
      type: "string",
      required: true
    },
    /** Version id mirrored from the canonical RoleAssignment row. */
    vid: {
      type: "string",
      required: true
    },
    /** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
    lastUpdated: {
      type: "string",
      required: true
    },
    /**
     * Denormalized Tenant display name — mirrored from the canonical
     * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
     * Optional on the schema because pre-TR-024 rows may not carry a
     * display name; the operations layer falls back gracefully when
     * missing.
     */
    denormalizedTenantName: {
      type: "string",
      required: false
    },
    /**
     * Denormalized User display name — mirrored from the canonical
     * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
     * Carried on the projection so consumers can render the user's
     * display name without a hop to the User record.
     */
    denormalizedUserName: {
      type: "string",
      required: false
    },
    /**
     * Denormalized Role display name — required to compose the SK's
     * `<normalizedRoleName>` segment. Optional on the schema (pre-TR-024
     * rows fall back to a sentinel) but expected to be present at write
     * time per TR-024 rule 2 (write-time source =
     * canonical Role.displayName).
     */
    denormalizedRoleName: {
      type: "string",
      required: false
    }
  },
  indexes: {
    /**
     * Base table: PK = USER#ID#\<userId\>, SK = operation-supplied. Both
     * sub-lanes (tenant-level and workspace-level) use this same index —
     * the SK string encodes the lane discriminator
     * (`ROLEASSIGNMENT#TENANT#…` vs `ROLEASSIGNMENT#WORKSPACE#…`) so a
     * single `Query(PK = USER#ID#<userId>, SK begins_with 'ROLEASSIGNMENT#')`
     * returns both lanes interleaved.
     */
    record: {
      pk: {
        field: "PK",
        composite: ["userId"],
        template: "USER#ID#${userId}"
      },
      sk: {
        field: "SK",
        casing: "none",
        composite: ["sk"],
        template: "${sk}"
      }
    }
  }
});

// src/data/dynamo/entities/control/roleassignment-workspace-projection-entity.ts
import { Entity as Entity10 } from "electrodb";
var RoleAssignmentWorkspaceProjectionEntity = new Entity10({
  model: {
    entity: "roleAssignmentWorkspaceProjection",
    service: "control",
    version: "01"
  },
  attributes: {
    /**
     * Tenant the workspace belongs to. Renders as the leading segment
     * of the base-table PK. Always required — the workspace partition
     * is tenant-scoped per ADR-011.
     */
    tenantId: {
      type: "string",
      required: true
    },
    /**
     * Workspace partition discriminator. Renders as the trailing
     * segment of the base-table PK
     * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
     * the projection has no meaning outside a workspace partition.
     */
    workspaceId: {
      type: "string",
      required: true
    },
    /**
     * Pre-composed sort key — built by the operations-layer projection
     * writer via `buildRoleAssignmentWorkspaceProjectionSk`. The entity
     * stores the value verbatim so the SK grammar (pattern #9) is
     * owned by the operations layer, not duplicated here.
     */
    sk: {
      type: "string",
      required: true
    },
    /**
     * User the role assignment grants the role to. Stored as a
     * discriminating field so consumers can hydrate the canonical User
     * row via `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
     * projection's `summary` is insufficient.
     */
    userId: {
      type: "string",
      required: true
    },
    /**
     * Role the assignment grants. Stored as a discriminating field —
     * also rendered into the SK as the discriminator-first segment so
     * `begins_with('ROLEASSIGNMENT#<roleId>#')` filters one role.
     */
    roleId: {
      type: "string",
      required: true
    },
    /**
     * RoleAssignment canonical-record id. Stored as a discriminating
     * field so consumers can hydrate the canonical row via
     * `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
     * when the projection's `summary` is insufficient.
     */
    roleAssignmentId: {
      type: "string",
      required: true
    },
    /**
     * Summary projection (key display fields as JSON string: id,
     * displayName, status) — mirrored from the canonical RoleAssignment
     * row so workspace-partition queries do not need a BatchGet hop.
     */
    summary: {
      type: "string",
      required: true
    },
    /** Version id mirrored from the canonical RoleAssignment row. */
    vid: {
      type: "string",
      required: true
    },
    /** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
    lastUpdated: {
      type: "string",
      required: true
    },
    /**
     * Denormalized User display name — required to compose the
     * pattern-#9 SK (`ROLEASSIGNMENT#<roleId>#<normalizedUserName>#…`).
     * Optional on the schema because pre-TR-024 rows may not carry a
     * display name; the operations layer falls back to a sentinel when
     * missing so the SK still has a valid shape. The TR-023 rename-
     * cascade pipeline rewrites the SK on a User rename.
     */
    denormalizedUserName: {
      type: "string",
      required: false
    },
    /**
     * Denormalized Role display name — mirrored from the canonical
     * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
     * Carried on the projection so consumers can render the role's
     * display name without a hop to the Role record. Not part of the
     * SK (pattern #9 sorts on `<normalizedUserName>`, not role name) —
     * a Role rename does NOT rewrite this SK.
     */
    denormalizedRoleName: {
      type: "string",
      required: false
    }
  },
  indexes: {
    /**
     * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
     * SK = operation-supplied. Pattern #9 uses this index — the SK
     * encodes the entity-type prefix and discriminator-first roleId
     * (`ROLEASSIGNMENT#<roleId>#…`) so
     * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'ROLEASSIGNMENT#<roleId>#')`
     * returns every user-assignment for that role in the workspace, sorted
     * by normalized user name.
     */
    record: {
      pk: {
        field: "PK",
        composite: ["tenantId", "workspaceId"],
        template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
      },
      sk: {
        field: "SK",
        casing: "none",
        composite: ["sk"],
        template: "${sk}"
      }
    }
  }
});

// src/data/dynamo/entities/control/tenant-entity.ts
import { Entity as Entity11 } from "electrodb";
var TenantEntity = new Entity11({
  model: {
    entity: "tenant",
    service: "control",
    version: "01"
  },
  attributes: {
    /** Sort key sentinel. Always "CURRENT". */
    sk: {
      type: "string",
      required: true,
      default: "CURRENT"
    },
    /** The tenant's own id (= resource id). Drives the partition key. */
    tenantId: {
      type: "string",
      required: true
    },
    /** FHIR Resource.id; logical id in URL. Equals tenantId. */
    id: {
      type: "string",
      required: true
    },
    /** Full Tenant resource serialized as JSON string. */
    resource: {
      type: "string",
      required: true
    },
    /**
     * Summary projection (key display fields as JSON string: id, displayName, status).
     * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
     */
    summary: {
      type: "string",
      required: true
    },
    /** Version id (e.g. ULID). */
    vid: {
      type: "string",
      required: true
    },
    lastUpdated: {
      type: "string",
      required: true
    },
    /**
     * ADR-028 denormalized counter — number of tenant-scoped Memberships
     * (users) in this tenant. Maintained by the counter-maintenance
     * consumer via atomic ADD; absent/0 until first event or reconciliation.
     */
    usersInTenant: {
      type: "number",
      required: false
    },
    /**
     * ADR-028 denormalized counter — number of Workspaces in this tenant.
     * Maintained by the counter-maintenance consumer via atomic ADD;
     * absent/0 until first event or reconciliation.
     */
    workspacesInTenant: {
      type: "number",
      required: false
    },
    gsi1Shard: gsi1ShardAttribute,
    /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
    gsi1sk: gsi1skAttribute,
    deleted: {
      type: "boolean",
      required: false
    },
    bundleId: {
      type: "string",
      required: false
    },
    msgId: {
      type: "string",
      required: false
    }
  },
  indexes: {
    /** Base table: PK = TENANT#ID#<tenantId>, SK = CURRENT. Do not supply PK or SK from outside. */
    record: {
      pk: {
        field: "PK",
        composite: ["tenantId"],
        template: "TENANT#ID#${tenantId}"
      },
      sk: {
        field: "SK",
        composite: ["sk"],
        template: "${sk}"
      }
    },
    /**
     * GSI1 — Unified Sharded List per ADR-011: list all Tenants across the four shards.
     * Tenant lives at the platform tier (no parent tenant or workspace), so `TID#-#WID#-`
     * sentinels precede `RT#Tenant#SHARD#<n>`. SK is derived via `gsi1skAttribute` —
     * `<normalizedName>#<id>` when the resource carries a `name`, else `<lastUpdated>#<id>`
     * (DR-004). `casing: "none"` preserves the normalized label and ISO-8601 `T`/`Z`.
     */
    gsi1: {
      index: "GSI1",
      pk: {
        field: "GSI1PK",
        composite: ["gsi1Shard"],
        template: "TID#-#WID#-#RT#Tenant#SHARD#${gsi1Shard}"
      },
      sk: {
        field: "GSI1SK",
        casing: "none",
        composite: ["gsi1sk"],
        template: "${gsi1sk}"
      }
    }
  }
});

// src/data/dynamo/entities/control/user-entity.ts
import { Entity as Entity12 } from "electrodb";
var UserEntity = new Entity12({
  model: {
    entity: "user",
    service: "control",
    version: "01"
  },
  attributes: {
    /** Sort key sentinel. Always "CURRENT". */
    sk: {
      type: "string",
      required: true,
      default: "CURRENT"
    },
    /** FHIR Resource.id; platform user id (ohi_uid). */
    id: {
      type: "string",
      required: true
    },
    /** Full User resource serialized as JSON string. */
    resource: {
      type: "string",
      required: true
    },
    /**
     * Summary projection (key display fields as JSON string: id, displayName, status).
     * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
     */
    summary: {
      type: "string",
      required: true
    },
    /**
     * Immutable Cognito-issued `sub` claim. Drives GSI2 (sub-lookup). Optional until the
     * Post Confirmation Lambda (#770) lands; required thereafter.
     */
    cognitoSub: {
      type: "string",
      required: false
    },
    /** Version id (e.g. ULID). */
    vid: {
      type: "string",
      required: true
    },
    lastUpdated: {
      type: "string",
      required: true
    },
    /**
     * ADR-028 denormalized counter — number of tenant-scoped Memberships
     * (tenants) this user belongs to. Maintained by the
     * counter-maintenance consumer via atomic ADD; absent/0 until first
     * event or reconciliation.
     */
    tenantsForUser: {
      type: "number",
      required: false
    },
    /**
     * ADR-028 denormalized counter — number of workspace-scoped
     * Memberships (workspaces) this user belongs to. Maintained by the
     * counter-maintenance consumer via atomic ADD; absent/0 until first
     * event or reconciliation.
     */
    workspacesForUser: {
      type: "number",
      required: false
    },
    gsi1Shard: gsi1ShardAttribute,
    /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
    gsi1sk: gsi1skAttribute,
    deleted: {
      type: "boolean",
      required: false
    },
    /**
     * TR-022 / ADR-018 lifecycle state for the cascade pipeline.
     *
     * - `active` (or undefined) — normal, readable state.
     * - `deleting` — intermediate state set synchronously by the
     *   hard-delete API entry point. The owning-delete cascade state
     *   machine fans out from this transition (DynamoDB stream →
     *   `control-plane.owning-delete.v1` → Step Functions). Readers MUST
     *   short-circuit on `deleting` so partial cascades stay invisible.
     * - `deleted-failed` — terminal failure state set by the cascade
     *   finalize Lambda when the cascade run fails irrecoverably.
     *   Operators recover by re-running the cascade or by direct
     *   intervention.
     *
     * The cascade finalize step deletes the canonical record conditional
     * on `lifecycleState = "deleting"`; on replay the conditional check
     * fails and the finalize step treats that as a no-op success.
     */
    lifecycleState: {
      type: ["active", "deleting", "deleted-failed"],
      required: false
    },
    bundleId: {
      type: "string",
      required: false
    },
    msgId: {
      type: "string",
      required: false
    }
  },
  indexes: {
    /** Base table: PK = USER#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
    record: {
      pk: {
        field: "PK",
        composite: ["id"],
        template: "USER#ID#${id}"
      },
      sk: {
        field: "SK",
        composite: ["sk"],
        template: "${sk}"
      }
    },
    /**
     * GSI1 — Unified Sharded List per ADR-011: list all Users across the four shards.
     * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#User#SHARD#<n>`.
     * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
     * extractable (string `name`/`title` via introspection), else `<lastUpdated>#<id>`
     * (DR-004). `casing: "none"` preserves the normalized label and ISO-8601 `T`/`Z`.
     */
    gsi1: {
      index: "GSI1",
      pk: {
        field: "GSI1PK",
        composite: ["gsi1Shard"],
        template: "TID#-#WID#-#RT#User#SHARD#${gsi1Shard}"
      },
      sk: {
        field: "GSI1SK",
        casing: "none",
        composite: ["gsi1sk"],
        template: "${gsi1sk}"
      }
    },
    /**
     * GSI2 — Cognito sub-lookup per ADR-011: resolves the UserEntity from a Cognito `sub` claim.
     * `condition` skips the index when `cognitoSub` is missing so legacy items without a sub are
     * not indexed.
     */
    gsi2: {
      index: "GSI2",
      condition: (attrs) => typeof attrs.cognitoSub === "string" && attrs.cognitoSub.length > 0,
      pk: {
        field: "GSI2PK",
        casing: "none",
        composite: ["cognitoSub"],
        template: "USER#SUB#${cognitoSub}"
      },
      sk: {
        field: "GSI2SK",
        casing: "none",
        composite: [],
        template: "CURRENT"
      }
    }
  }
});

// src/data/dynamo/entities/control/workspace-entity.ts
import { Entity as Entity13 } from "electrodb";
var WorkspaceEntity = new Entity13({
  model: {
    entity: "workspace",
    service: "control",
    version: "01"
  },
  attributes: {
    /** Sort key sentinel. Always "CURRENT". */
    sk: {
      type: "string",
      required: true,
      default: "CURRENT"
    },
    /** Tenant that contains this workspace (required). */
    tenantId: {
      type: "string",
      required: true
    },
    /** FHIR Resource.id; logical id in URL. */
    id: {
      type: "string",
      required: true
    },
    /** Full Workspace resource serialized as JSON string. */
    resource: {
      type: "string",
      required: true
    },
    /**
     * Summary projection (key display fields as JSON string: id, displayName, status).
     * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
     */
    summary: {
      type: "string",
      required: true
    },
    /** Version id (e.g. ULID). */
    vid: {
      type: "string",
      required: true
    },
    lastUpdated: {
      type: "string",
      required: true
    },
    /**
     * ADR-028 denormalized counter — number of workspace-scoped
     * Memberships (users) in this workspace. Maintained by the
     * counter-maintenance consumer via atomic ADD; absent/0 until first
     * event or reconciliation.
     */
    usersInWorkspace: {
      type: "number",
      required: false
    },
    /**
     * ADR-028 denormalized counter — number of workspace-scoped
     * RoleAssignments classified as admin-tier in this workspace.
     * Maintained by the counter-maintenance consumer via atomic ADD;
     * absent/0 until first event or reconciliation.
     */
    adminUsersInWorkspace: {
      type: "number",
      required: false
    },
    /**
     * ADR-028 denormalized counter — number of workspace-scoped
     * RoleAssignments classified as non-admin in this workspace.
     * Maintained by the counter-maintenance consumer via atomic ADD;
     * absent/0 until first event or reconciliation.
     */
    normalUsersInWorkspace: {
      type: "number",
      required: false
    },
    gsi1Shard: gsi1ShardAttribute,
    /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
    gsi1sk: gsi1skAttribute,
    deleted: {
      type: "boolean",
      required: false
    },
    /**
     * TR-022 / ADR-018 lifecycle state for the cascade pipeline.
     *
     * - `active` (or undefined) — normal, readable state.
     * - `deleting` — intermediate state set synchronously by the
     *   hard-delete API entry point. The owning-delete cascade state
     *   machine fans out from this transition (DynamoDB stream →
     *   `control-plane.owning-delete.v1` → Step Functions). Readers MUST
     *   short-circuit on `deleting` so partial cascades stay invisible.
     * - `deleted-failed` — terminal failure state set by the cascade
     *   finalize Lambda when the cascade run fails irrecoverably.
     *   Operators recover by re-running the cascade or by direct
     *   intervention.
     *
     * The cascade finalize step deletes the canonical record conditional
     * on `lifecycleState = "deleting"`; on replay the conditional check
     * fails and the finalize step treats that as a no-op success.
     */
    lifecycleState: {
      type: ["active", "deleting", "deleted-failed"],
      required: false
    },
    bundleId: {
      type: "string",
      required: false
    },
    msgId: {
      type: "string",
      required: false
    }
  },
  indexes: {
    /** Base table: PK = TID#<tenantId>#WORKSPACE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
    record: {
      pk: {
        field: "PK",
        composite: ["tenantId", "id"],
        template: "TID#${tenantId}#WORKSPACE#ID#${id}"
      },
      sk: {
        field: "SK",
        composite: ["sk"],
        template: "${sk}"
      }
    },
    /**
     * GSI1 — Unified Sharded List per ADR-011: list all Workspaces for a tenant across the
     * four shards. Workspace is itself the workspace identity, so `WID#-` is a sentinel.
     * SK is derived via `gsi1skAttribute` — `<normalizedName>#<id>` when the resource
     * carries a `name`, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves
     * the normalized label and ISO-8601 `T`/`Z`.
     */
    gsi1: {
      index: "GSI1",
      pk: {
        field: "GSI1PK",
        composite: ["tenantId", "gsi1Shard"],
        template: "TID#${tenantId}#WID#-#RT#Workspace#SHARD#${gsi1Shard}"
      },
      sk: {
        field: "GSI1SK",
        casing: "none",
        composite: ["gsi1sk"],
        template: "${gsi1sk}"
      }
    }
  }
});

// src/data/dynamo/dynamo-control-service.ts
var controlPlaneEntities = {
  configuration: ConfigurationEntity,
  configurationUserProjection: ConfigurationUserProjectionEntity,
  configurationWorkspaceProjection: ConfigurationWorkspaceProjectionEntity,
  membership: MembershipEntity,
  membershipUserProjection: MembershipUserProjectionEntity,
  membershipWorkspaceProjection: MembershipWorkspaceProjectionEntity,
  role: RoleEntity,
  roleAssignment: RoleAssignmentEntity,
  roleAssignmentUserProjection: RoleAssignmentUserProjectionEntity,
  roleAssignmentWorkspaceProjection: RoleAssignmentWorkspaceProjectionEntity,
  tenant: TenantEntity,
  user: UserEntity,
  workspace: WorkspaceEntity
};
var controlPlaneService = new Service(controlPlaneEntities, {
  table: defaultTableName,
  client: dynamoClient
});
var DynamoControlService = {
  entities: controlPlaneService.entities,
  transaction: controlPlaneService.transaction
};
function getDynamoControlService(tableName) {
  const resolved = tableName ?? defaultTableName;
  const service = new Service(controlPlaneEntities, {
    table: resolved,
    client: dynamoClient
  });
  return {
    entities: service.entities,
    transaction: service.transaction
  };
}

export {
  SHARD_COUNT,
  computeShard,
  getDynamoControlService
};
//# sourceMappingURL=chunk-EUIP2U5F.mjs.map