"""Identity-consumer self-service request flow. Adds ``identity_consumer_requests`` — the application a system owner submits to register an identity consumer. On submit Helix creates a Feishu approval instance (flow_key ``identity_consumer_request``); on approval the callback materialises the live consumer + a one-time token-issue link. Also seeds the ``integration_approval_flows`` row mapping that flow_key to the Feishu template the admin created (external_code ``413C2807-6DDF-4669-A37B-0A4D651DC313``), bound to the primary Feishu chat_approval integration. Best-effort + idempotent (mirrors migration 149): skipped on a fresh DB with no Feishu integration; never clobbers an admin-disabled row on re-run. FK collation: ``identity_consumer_requests`` is built with the parent's charset/collation and references ``identity_persons`` / ``identity_consumers`` out-of-line so the inline FKs can't trip error 3780 on prod TiDB. """ from __future__ import annotations import uuid import sqlalchemy as sa from alembic import op from app.core.migration_helpers import id_type_for, table_kw_for revision = "286" down_revision = "285" branch_labels = None depends_on = None TABLE = "identity_consumer_requests" _FLOW_KEY = "identity_consumer_request" _FLOW_LABEL = "身份消费方接入申请" _EXTERNAL_CODE = "413C2807-6DDF-4669-A37B-0A4D651DC313" def _table_exists(bind, name: str) -> bool: return bind.execute( sa.text( "SELECT 1 FROM information_schema.TABLES " "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :n" ), {"n": name}, ).scalar() is not None def upgrade() -> None: bind = op.get_bind() persons_id = id_type_for("identity_persons") consumers_id = id_type_for("identity_consumers") kw = table_kw_for("identity_persons") if not _table_exists(bind, TABLE): op.create_table( TABLE, sa.Column("id", persons_id, primary_key=True), sa.Column("requester_person_id", persons_id, nullable=False), sa.Column("name", sa.String(128), nullable=False), sa.Column("slug", sa.String(64), nullable=False), sa.Column("description", sa.Text(), nullable=True), sa.Column("owner_person_id", persons_id, nullable=False), sa.Column("scopes", sa.JSON(), nullable=False), sa.Column("granted_fields", sa.JSON(), nullable=False), sa.Column("filter_config", sa.JSON(), nullable=False), sa.Column("rate_limit_per_minute", sa.Integer(), nullable=True), sa.Column("token_validity_days", sa.Integer(), nullable=True), sa.Column("pii_justification", sa.Text(), nullable=True), sa.Column( "status", sa.String(24), nullable=False, server_default="pending_approval", ), sa.Column("reject_reason", sa.Text(), nullable=True), sa.Column("feishu_instance_code", sa.String(128), nullable=True), sa.Column("approval_request_id", persons_id, nullable=True), sa.Column("consumer_id", consumers_id, nullable=True), sa.Column("decided_at", sa.DateTime(timezone=True), nullable=True), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP"), ), sa.Column( "updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP"), ), sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), **kw, ) # Out-of-line FKs after the table is built with the parent's # collation (avoids inline-FK 3780 on prod TiDB). op.create_foreign_key( "fk_idconsumer_req_requester", TABLE, "identity_persons", ["requester_person_id"], ["id"], ) op.create_foreign_key( "fk_idconsumer_req_owner", TABLE, "identity_persons", ["owner_person_id"], ["id"], ) op.create_foreign_key( "fk_idconsumer_req_consumer", TABLE, "identity_consumers", ["consumer_id"], ["id"], ) op.create_index( "idx_identity_consumer_requests_requester", TABLE, ["requester_person_id"], ) op.create_index( "idx_identity_consumer_requests_status", TABLE, ["status"], ) _seed_flow(bind) def _seed_flow(bind) -> None: row = bind.execute( sa.text( "SELECT id FROM integrations " "WHERE kind = 'chat_approval' AND provider = 'feishu' " " AND enabled = 1 " "ORDER BY is_primary DESC, created_at ASC LIMIT 1" ) ).fetchone() if not row: return # fresh DB / no Feishu — admin configures later integration_id = row[0] existing = bind.execute( sa.text( "SELECT id FROM integration_approval_flows " "WHERE integration_id = :int_id AND flow_key = :flow_key" ), {"int_id": integration_id, "flow_key": _FLOW_KEY}, ).fetchone() if existing: return # already configured (manual prod seed) — don't clobber bind.execute( sa.text( "INSERT INTO integration_approval_flows " "(id, integration_id, flow_key, label, external_code, active) " "VALUES (:id, :int_id, :flow_key, :label, :code, 1)" ), { "id": str(uuid.uuid4()), "int_id": integration_id, "flow_key": _FLOW_KEY, "label": _FLOW_LABEL, "code": _EXTERNAL_CODE, }, ) def downgrade() -> None: bind = op.get_bind() bind.execute( sa.text( "DELETE FROM integration_approval_flows WHERE flow_key = :flow_key" ), {"flow_key": _FLOW_KEY}, ) if _table_exists(bind, TABLE): op.drop_table(TABLE)