refactor: update migration paths and improve database operation handling
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
## Project Structure & Module Organization
|
||||
- `backend/`: FastAPI service.
|
||||
- App code: `backend/app/` (routes `backend/app/api/`, models `backend/app/models/`, schemas `backend/app/schemas/`, workers `backend/app/workers/`).
|
||||
- DB migrations: `backend/alembic/` (generated versions in `backend/alembic/versions/`).
|
||||
- DB migrations: `backend/migrations/` (generated versions in `backend/migrations/versions/`).
|
||||
- Tests: `backend/tests/`.
|
||||
- `frontend/`: Next.js app.
|
||||
- Routes: `frontend/src/app/`; shared UI: `frontend/src/components/`; utilities: `frontend/src/lib/`.
|
||||
|
||||
2
Makefile
2
Makefile
@@ -101,7 +101,7 @@ frontend-test: frontend-tooling ## Frontend tests (vitest)
|
||||
$(NODE_WRAP) --cwd $(FRONTEND_DIR) npm run test
|
||||
|
||||
.PHONY: backend-migrate
|
||||
backend-migrate: ## Apply backend DB migrations (alembic upgrade head)
|
||||
backend-migrate: ## Apply backend DB migrations (uses backend/migrations)
|
||||
cd $(BACKEND_DIR) && uv run alembic upgrade head
|
||||
|
||||
.PHONY: build
|
||||
|
||||
@@ -4,7 +4,7 @@ extend-ignore = E203, W503, E501
|
||||
exclude =
|
||||
.venv,
|
||||
backend/.venv,
|
||||
alembic,
|
||||
backend/alembic,
|
||||
migrations,
|
||||
backend/migrations,
|
||||
**/__pycache__,
|
||||
**/*.pyc
|
||||
|
||||
@@ -34,7 +34,7 @@ COPY --from=deps /app/.venv /app/.venv
|
||||
ENV PATH="/app/.venv/bin:${PATH}"
|
||||
|
||||
# Copy app source
|
||||
COPY backend/alembic ./alembic
|
||||
COPY backend/migrations ./migrations
|
||||
COPY backend/alembic.ini ./alembic.ini
|
||||
COPY backend/app ./app
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ Clerk is used for user authentication (optional for local/self-host in many setu
|
||||
|
||||
## Database migrations (Alembic)
|
||||
|
||||
Migrations live in `backend/alembic/versions/*`.
|
||||
Migrations live in `backend/migrations/versions/*`.
|
||||
|
||||
Common commands:
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[alembic]
|
||||
script_location = alembic
|
||||
script_location = migrations
|
||||
prepend_sys_path = .
|
||||
sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
"""backfill_invite_access
|
||||
|
||||
Revision ID: 050c16fde00e
|
||||
Revises: 2c7b1c4d9e10
|
||||
Create Date: 2026-02-08 20:07:14.621575
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '050c16fde00e'
|
||||
down_revision = '2c7b1c4d9e10'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
now = datetime.utcnow()
|
||||
rows = bind.execute(
|
||||
sa.text(
|
||||
"""
|
||||
SELECT
|
||||
m.id AS member_id,
|
||||
iba.board_id AS board_id,
|
||||
iba.can_read AS can_read,
|
||||
iba.can_write AS can_write
|
||||
FROM organization_invites i
|
||||
JOIN organization_invite_board_access iba
|
||||
ON iba.organization_invite_id = i.id
|
||||
JOIN organization_members m
|
||||
ON m.user_id = i.accepted_by_user_id
|
||||
AND m.organization_id = i.organization_id
|
||||
WHERE i.accepted_at IS NOT NULL
|
||||
"""
|
||||
)
|
||||
).fetchall()
|
||||
|
||||
for row in rows:
|
||||
can_write = bool(row.can_write)
|
||||
can_read = bool(row.can_read or row.can_write)
|
||||
bind.execute(
|
||||
sa.text(
|
||||
"""
|
||||
INSERT INTO organization_board_access (
|
||||
id,
|
||||
organization_member_id,
|
||||
board_id,
|
||||
can_read,
|
||||
can_write,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
:id,
|
||||
:member_id,
|
||||
:board_id,
|
||||
:can_read,
|
||||
:can_write,
|
||||
:now,
|
||||
:now
|
||||
)
|
||||
ON CONFLICT (organization_member_id, board_id) DO UPDATE
|
||||
SET
|
||||
can_read = organization_board_access.can_read OR EXCLUDED.can_read,
|
||||
can_write = organization_board_access.can_write OR EXCLUDED.can_write,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": uuid.uuid4(),
|
||||
"member_id": row.member_id,
|
||||
"board_id": row.board_id,
|
||||
"can_read": can_read,
|
||||
"can_write": can_write,
|
||||
"now": now,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -1,54 +0,0 @@
|
||||
"""board groups
|
||||
|
||||
Revision ID: 12772fdcdfe9
|
||||
Revises: 9f0c4fb2a7b8
|
||||
Create Date: 2026-02-07 17:13:50.597099
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "12772fdcdfe9"
|
||||
down_revision = "9f0c4fb2a7b8"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"board_groups",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("slug", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("ix_board_groups_slug", "board_groups", ["slug"], unique=False)
|
||||
|
||||
op.add_column("boards", sa.Column("board_group_id", sa.Uuid(), nullable=True))
|
||||
op.create_index("ix_boards_board_group_id", "boards", ["board_group_id"], unique=False)
|
||||
op.create_foreign_key(
|
||||
"fk_boards_board_group_id_board_groups",
|
||||
"boards",
|
||||
"board_groups",
|
||||
["board_group_id"],
|
||||
["id"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_constraint(
|
||||
"fk_boards_board_group_id_board_groups", "boards", type_="foreignkey"
|
||||
)
|
||||
op.drop_index("ix_boards_board_group_id", table_name="boards")
|
||||
op.drop_column("boards", "board_group_id")
|
||||
|
||||
op.drop_index("ix_board_groups_slug", table_name="board_groups")
|
||||
op.drop_table("board_groups")
|
||||
@@ -1,388 +0,0 @@
|
||||
"""init (squashed)
|
||||
|
||||
Revision ID: 1d844b04ee06
|
||||
Revises:
|
||||
Create Date: 2026-02-06
|
||||
|
||||
This is a squashed init migration representing the current schema at revision
|
||||
`1d844b04ee06`.
|
||||
|
||||
Note: older Alembic revision files were consolidated into this single revision.
|
||||
Databases already stamped/applied at `1d844b04ee06` will remain compatible.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "1d844b04ee06"
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"gateways",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("url", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("token", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("main_session_key", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("workspace_root", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column(
|
||||
"skyll_enabled",
|
||||
sa.Boolean(),
|
||||
server_default=sa.text("false"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"boards",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("slug", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("gateway_id", sa.Uuid(), nullable=True),
|
||||
sa.Column(
|
||||
"board_type",
|
||||
sa.String(),
|
||||
server_default="goal",
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("objective", sa.Text(), nullable=True),
|
||||
sa.Column("success_metrics", sa.JSON(), nullable=True),
|
||||
sa.Column("target_date", sa.DateTime(), nullable=True),
|
||||
sa.Column(
|
||||
"goal_confirmed",
|
||||
sa.Boolean(),
|
||||
server_default=sa.text("false"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("goal_source", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["gateway_id"], ["gateways.id"]),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("ix_boards_slug", "boards", ["slug"], unique=False)
|
||||
op.create_index("ix_boards_gateway_id", "boards", ["gateway_id"], unique=False)
|
||||
op.create_index("ix_boards_board_type", "boards", ["board_type"], unique=False)
|
||||
|
||||
op.create_table(
|
||||
"users",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("clerk_user_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("email", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("preferred_name", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("pronouns", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("timezone", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("notes", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("context", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("is_super_admin", sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("ix_users_clerk_user_id", "users", ["clerk_user_id"], unique=True)
|
||||
op.create_index("ix_users_email", "users", ["email"], unique=False)
|
||||
|
||||
op.create_table(
|
||||
"agents",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("board_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("status", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("openclaw_session_id", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("agent_token_hash", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("heartbeat_config", sa.JSON(), nullable=True),
|
||||
sa.Column("identity_profile", sa.JSON(), nullable=True),
|
||||
sa.Column("identity_template", sa.Text(), nullable=True),
|
||||
sa.Column("soul_template", sa.Text(), nullable=True),
|
||||
sa.Column("provision_requested_at", sa.DateTime(), nullable=True),
|
||||
sa.Column(
|
||||
"provision_confirm_token_hash",
|
||||
sqlmodel.sql.sqltypes.AutoString(),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column("provision_action", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("delete_requested_at", sa.DateTime(), nullable=True),
|
||||
sa.Column(
|
||||
"delete_confirm_token_hash",
|
||||
sqlmodel.sql.sqltypes.AutoString(),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column("last_seen_at", sa.DateTime(), nullable=True),
|
||||
sa.Column(
|
||||
"is_board_lead",
|
||||
sa.Boolean(),
|
||||
server_default=sa.text("false"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["board_id"], ["boards.id"]),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("ix_agents_board_id", "agents", ["board_id"], unique=False)
|
||||
op.create_index("ix_agents_name", "agents", ["name"], unique=False)
|
||||
op.create_index("ix_agents_status", "agents", ["status"], unique=False)
|
||||
op.create_index(
|
||||
"ix_agents_openclaw_session_id", "agents", ["openclaw_session_id"], unique=False
|
||||
)
|
||||
op.create_index("ix_agents_agent_token_hash", "agents", ["agent_token_hash"], unique=False)
|
||||
op.create_index(
|
||||
"ix_agents_provision_confirm_token_hash",
|
||||
"agents",
|
||||
["provision_confirm_token_hash"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index("ix_agents_provision_action", "agents", ["provision_action"], unique=False)
|
||||
op.create_index(
|
||||
"ix_agents_delete_confirm_token_hash",
|
||||
"agents",
|
||||
["delete_confirm_token_hash"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index("ix_agents_is_board_lead", "agents", ["is_board_lead"], unique=False)
|
||||
|
||||
op.create_table(
|
||||
"tasks",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("board_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("title", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("status", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("priority", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("due_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("in_progress_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("created_by_user_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("assigned_agent_id", sa.Uuid(), nullable=True),
|
||||
sa.Column(
|
||||
"auto_created",
|
||||
sa.Boolean(),
|
||||
server_default=sa.text("false"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("auto_reason", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["assigned_agent_id"], ["agents.id"]),
|
||||
sa.ForeignKeyConstraint(["board_id"], ["boards.id"]),
|
||||
sa.ForeignKeyConstraint(["created_by_user_id"], ["users.id"]),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("ix_tasks_board_id", "tasks", ["board_id"], unique=False)
|
||||
op.create_index("ix_tasks_status", "tasks", ["status"], unique=False)
|
||||
op.create_index("ix_tasks_priority", "tasks", ["priority"], unique=False)
|
||||
op.create_index("ix_tasks_due_at", "tasks", ["due_at"], unique=False)
|
||||
op.create_index("ix_tasks_assigned_agent_id", "tasks", ["assigned_agent_id"], unique=False)
|
||||
op.create_index("ix_tasks_created_by_user_id", "tasks", ["created_by_user_id"], unique=False)
|
||||
|
||||
op.create_table(
|
||||
"activity_events",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("event_type", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("message", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("agent_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("task_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["agent_id"], ["agents.id"]),
|
||||
sa.ForeignKeyConstraint(["task_id"], ["tasks.id"]),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("ix_activity_events_event_type", "activity_events", ["event_type"], unique=False)
|
||||
op.create_index("ix_activity_events_agent_id", "activity_events", ["agent_id"], unique=False)
|
||||
op.create_index("ix_activity_events_task_id", "activity_events", ["task_id"], unique=False)
|
||||
|
||||
op.create_table(
|
||||
"board_memory",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("board_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("content", sa.Text(), nullable=False),
|
||||
sa.Column("tags", sa.JSON(), nullable=True),
|
||||
sa.Column(
|
||||
"is_chat",
|
||||
sa.Boolean(),
|
||||
server_default=sa.text("false"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("source", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["board_id"], ["boards.id"]),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("ix_board_memory_board_id", "board_memory", ["board_id"], unique=False)
|
||||
op.create_index("ix_board_memory_is_chat", "board_memory", ["is_chat"], unique=False)
|
||||
op.create_index(
|
||||
"ix_board_memory_board_id_is_chat_created_at",
|
||||
"board_memory",
|
||||
["board_id", "is_chat", "created_at"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"approvals",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("board_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("task_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("agent_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("action_type", sa.String(), nullable=False),
|
||||
sa.Column("payload", sa.JSON(), nullable=True),
|
||||
sa.Column("confidence", sa.Integer(), nullable=False),
|
||||
sa.Column("rubric_scores", sa.JSON(), nullable=True),
|
||||
sa.Column("status", sa.String(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("resolved_at", sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(["agent_id"], ["agents.id"]),
|
||||
sa.ForeignKeyConstraint(["board_id"], ["boards.id"]),
|
||||
sa.ForeignKeyConstraint(["task_id"], ["tasks.id"], ondelete="SET NULL"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("ix_approvals_board_id", "approvals", ["board_id"], unique=False)
|
||||
op.create_index("ix_approvals_agent_id", "approvals", ["agent_id"], unique=False)
|
||||
op.create_index("ix_approvals_task_id", "approvals", ["task_id"], unique=False)
|
||||
op.create_index("ix_approvals_status", "approvals", ["status"], unique=False)
|
||||
|
||||
op.create_table(
|
||||
"board_onboarding_sessions",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("board_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("session_key", sa.String(), nullable=False),
|
||||
sa.Column(
|
||||
"status",
|
||||
sa.String(),
|
||||
server_default="active",
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("messages", sa.JSON(), nullable=True),
|
||||
sa.Column("draft_goal", sa.JSON(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["board_id"], ["boards.id"]),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_board_onboarding_sessions_board_id",
|
||||
"board_onboarding_sessions",
|
||||
["board_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_board_onboarding_sessions_status",
|
||||
"board_onboarding_sessions",
|
||||
["status"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"task_fingerprints",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("board_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("fingerprint_hash", sa.String(), nullable=False),
|
||||
sa.Column("task_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["board_id"], ["boards.id"]),
|
||||
sa.ForeignKeyConstraint(["task_id"], ["tasks.id"]),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_task_fingerprints_board_hash",
|
||||
"task_fingerprints",
|
||||
["board_id", "fingerprint_hash"],
|
||||
unique=True,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_task_fingerprints_board_id",
|
||||
"task_fingerprints",
|
||||
["board_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_task_fingerprints_fingerprint_hash",
|
||||
"task_fingerprints",
|
||||
["fingerprint_hash"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_task_fingerprints_task_id",
|
||||
"task_fingerprints",
|
||||
["task_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_task_fingerprints_task_id", table_name="task_fingerprints")
|
||||
op.drop_index(
|
||||
"ix_task_fingerprints_fingerprint_hash",
|
||||
table_name="task_fingerprints",
|
||||
)
|
||||
op.drop_index("ix_task_fingerprints_board_id", table_name="task_fingerprints")
|
||||
op.drop_index("ix_task_fingerprints_board_hash", table_name="task_fingerprints")
|
||||
op.drop_table("task_fingerprints")
|
||||
|
||||
op.drop_index(
|
||||
"ix_board_onboarding_sessions_status",
|
||||
table_name="board_onboarding_sessions",
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_board_onboarding_sessions_board_id",
|
||||
table_name="board_onboarding_sessions",
|
||||
)
|
||||
op.drop_table("board_onboarding_sessions")
|
||||
|
||||
op.drop_index("ix_approvals_status", table_name="approvals")
|
||||
op.drop_index("ix_approvals_task_id", table_name="approvals")
|
||||
op.drop_index("ix_approvals_agent_id", table_name="approvals")
|
||||
op.drop_index("ix_approvals_board_id", table_name="approvals")
|
||||
op.drop_table("approvals")
|
||||
|
||||
op.drop_index(
|
||||
"ix_board_memory_board_id_is_chat_created_at",
|
||||
table_name="board_memory",
|
||||
)
|
||||
op.drop_index("ix_board_memory_is_chat", table_name="board_memory")
|
||||
op.drop_index("ix_board_memory_board_id", table_name="board_memory")
|
||||
op.drop_table("board_memory")
|
||||
|
||||
op.drop_index("ix_activity_events_task_id", table_name="activity_events")
|
||||
op.drop_index("ix_activity_events_agent_id", table_name="activity_events")
|
||||
op.drop_index("ix_activity_events_event_type", table_name="activity_events")
|
||||
op.drop_table("activity_events")
|
||||
|
||||
op.drop_index("ix_tasks_created_by_user_id", table_name="tasks")
|
||||
op.drop_index("ix_tasks_assigned_agent_id", table_name="tasks")
|
||||
op.drop_index("ix_tasks_due_at", table_name="tasks")
|
||||
op.drop_index("ix_tasks_priority", table_name="tasks")
|
||||
op.drop_index("ix_tasks_status", table_name="tasks")
|
||||
op.drop_index("ix_tasks_board_id", table_name="tasks")
|
||||
op.drop_table("tasks")
|
||||
|
||||
op.drop_index("ix_agents_is_board_lead", table_name="agents")
|
||||
op.drop_index("ix_agents_delete_confirm_token_hash", table_name="agents")
|
||||
op.drop_index("ix_agents_provision_action", table_name="agents")
|
||||
op.drop_index("ix_agents_provision_confirm_token_hash", table_name="agents")
|
||||
op.drop_index("ix_agents_agent_token_hash", table_name="agents")
|
||||
op.drop_index("ix_agents_openclaw_session_id", table_name="agents")
|
||||
op.drop_index("ix_agents_status", table_name="agents")
|
||||
op.drop_index("ix_agents_name", table_name="agents")
|
||||
op.drop_index("ix_agents_board_id", table_name="agents")
|
||||
op.drop_table("agents")
|
||||
|
||||
op.drop_index("ix_users_email", table_name="users")
|
||||
op.drop_index("ix_users_clerk_user_id", table_name="users")
|
||||
op.drop_table("users")
|
||||
|
||||
op.drop_index("ix_boards_board_type", table_name="boards")
|
||||
op.drop_index("ix_boards_gateway_id", table_name="boards")
|
||||
op.drop_index("ix_boards_slug", table_name="boards")
|
||||
op.drop_table("boards")
|
||||
|
||||
op.drop_table("gateways")
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
"""add organizations
|
||||
|
||||
Revision ID: 1f2a3b4c5d6e
|
||||
Revises: 9f0c4fb2a7b8
|
||||
Create Date: 2026-02-07
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "1f2a3b4c5d6e"
|
||||
down_revision = "9f0c4fb2a7b8"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
DEFAULT_ORG_NAME = "Personal"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"organizations",
|
||||
sa.Column("id", sa.UUID(), primary_key=True, nullable=False),
|
||||
sa.Column("name", sa.String(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.UniqueConstraint("name", name="uq_organizations_name"),
|
||||
)
|
||||
op.create_index("ix_organizations_name", "organizations", ["name"])
|
||||
|
||||
op.create_table(
|
||||
"organization_members",
|
||||
sa.Column("id", sa.UUID(), primary_key=True, nullable=False),
|
||||
sa.Column("organization_id", sa.UUID(), nullable=False),
|
||||
sa.Column("user_id", sa.UUID(), nullable=False),
|
||||
sa.Column("role", sa.String(), nullable=False, server_default="member"),
|
||||
sa.Column("all_boards_read", sa.Boolean(), nullable=False, server_default=sa.text("false")),
|
||||
sa.Column("all_boards_write", sa.Boolean(), nullable=False, server_default=sa.text("false")),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"], name="fk_org_members_org"),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], name="fk_org_members_user"),
|
||||
sa.UniqueConstraint(
|
||||
"organization_id",
|
||||
"user_id",
|
||||
name="uq_organization_members_org_user",
|
||||
),
|
||||
)
|
||||
op.create_index("ix_org_members_org", "organization_members", ["organization_id"])
|
||||
op.create_index("ix_org_members_user", "organization_members", ["user_id"])
|
||||
op.create_index("ix_org_members_role", "organization_members", ["role"])
|
||||
|
||||
op.create_table(
|
||||
"organization_board_access",
|
||||
sa.Column("id", sa.UUID(), primary_key=True, nullable=False),
|
||||
sa.Column("organization_member_id", sa.UUID(), nullable=False),
|
||||
sa.Column("board_id", sa.UUID(), nullable=False),
|
||||
sa.Column("can_read", sa.Boolean(), nullable=False, server_default=sa.text("true")),
|
||||
sa.Column("can_write", sa.Boolean(), nullable=False, server_default=sa.text("false")),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["organization_member_id"],
|
||||
["organization_members.id"],
|
||||
name="fk_org_board_access_member",
|
||||
),
|
||||
sa.ForeignKeyConstraint(["board_id"], ["boards.id"], name="fk_org_board_access_board"),
|
||||
sa.UniqueConstraint(
|
||||
"organization_member_id",
|
||||
"board_id",
|
||||
name="uq_org_board_access_member_board",
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_org_board_access_member",
|
||||
"organization_board_access",
|
||||
["organization_member_id"],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_org_board_access_board",
|
||||
"organization_board_access",
|
||||
["board_id"],
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"organization_invites",
|
||||
sa.Column("id", sa.UUID(), primary_key=True, nullable=False),
|
||||
sa.Column("organization_id", sa.UUID(), nullable=False),
|
||||
sa.Column("invited_email", sa.String(), nullable=False),
|
||||
sa.Column("token", sa.String(), nullable=False),
|
||||
sa.Column("role", sa.String(), nullable=False, server_default="member"),
|
||||
sa.Column("all_boards_read", sa.Boolean(), nullable=False, server_default=sa.text("false")),
|
||||
sa.Column("all_boards_write", sa.Boolean(), nullable=False, server_default=sa.text("false")),
|
||||
sa.Column("created_by_user_id", sa.UUID(), nullable=True),
|
||||
sa.Column("accepted_by_user_id", sa.UUID(), nullable=True),
|
||||
sa.Column("accepted_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"], name="fk_org_invites_org"),
|
||||
sa.ForeignKeyConstraint(["created_by_user_id"], ["users.id"], name="fk_org_invites_creator"),
|
||||
sa.ForeignKeyConstraint(["accepted_by_user_id"], ["users.id"], name="fk_org_invites_acceptor"),
|
||||
sa.UniqueConstraint("token", name="uq_org_invites_token"),
|
||||
)
|
||||
op.create_index("ix_org_invites_org", "organization_invites", ["organization_id"])
|
||||
op.create_index("ix_org_invites_email", "organization_invites", ["invited_email"])
|
||||
op.create_index("ix_org_invites_token", "organization_invites", ["token"])
|
||||
|
||||
op.create_table(
|
||||
"organization_invite_board_access",
|
||||
sa.Column("id", sa.UUID(), primary_key=True, nullable=False),
|
||||
sa.Column("organization_invite_id", sa.UUID(), nullable=False),
|
||||
sa.Column("board_id", sa.UUID(), nullable=False),
|
||||
sa.Column("can_read", sa.Boolean(), nullable=False, server_default=sa.text("true")),
|
||||
sa.Column("can_write", sa.Boolean(), nullable=False, server_default=sa.text("false")),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["organization_invite_id"],
|
||||
["organization_invites.id"],
|
||||
name="fk_org_invite_access_invite",
|
||||
),
|
||||
sa.ForeignKeyConstraint(["board_id"], ["boards.id"], name="fk_org_invite_access_board"),
|
||||
sa.UniqueConstraint(
|
||||
"organization_invite_id",
|
||||
"board_id",
|
||||
name="uq_org_invite_board_access_invite_board",
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_org_invite_access_invite",
|
||||
"organization_invite_board_access",
|
||||
["organization_invite_id"],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_org_invite_access_board",
|
||||
"organization_invite_board_access",
|
||||
["board_id"],
|
||||
)
|
||||
|
||||
op.add_column("boards", sa.Column("organization_id", sa.UUID(), nullable=True))
|
||||
op.add_column("board_groups", sa.Column("organization_id", sa.UUID(), nullable=True))
|
||||
op.add_column("gateways", sa.Column("organization_id", sa.UUID(), nullable=True))
|
||||
|
||||
op.create_index("ix_boards_organization_id", "boards", ["organization_id"])
|
||||
op.create_index("ix_board_groups_organization_id", "board_groups", ["organization_id"])
|
||||
op.create_index("ix_gateways_organization_id", "gateways", ["organization_id"])
|
||||
|
||||
op.create_foreign_key(
|
||||
"fk_boards_organization_id",
|
||||
"boards",
|
||||
"organizations",
|
||||
["organization_id"],
|
||||
["id"],
|
||||
)
|
||||
op.create_foreign_key(
|
||||
"fk_board_groups_organization_id",
|
||||
"board_groups",
|
||||
"organizations",
|
||||
["organization_id"],
|
||||
["id"],
|
||||
)
|
||||
op.create_foreign_key(
|
||||
"fk_gateways_organization_id",
|
||||
"gateways",
|
||||
"organizations",
|
||||
["organization_id"],
|
||||
["id"],
|
||||
)
|
||||
|
||||
bind = op.get_bind()
|
||||
now = datetime.utcnow()
|
||||
org_id = uuid.uuid4()
|
||||
bind.execute(
|
||||
sa.text(
|
||||
"INSERT INTO organizations (id, name, created_at, updated_at) VALUES (:id, :name, :now, :now)"
|
||||
),
|
||||
{"id": org_id, "name": DEFAULT_ORG_NAME, "now": now},
|
||||
)
|
||||
|
||||
bind.execute(
|
||||
sa.text("UPDATE boards SET organization_id = :org_id"),
|
||||
{"org_id": org_id},
|
||||
)
|
||||
bind.execute(
|
||||
sa.text("UPDATE board_groups SET organization_id = :org_id"),
|
||||
{"org_id": org_id},
|
||||
)
|
||||
bind.execute(
|
||||
sa.text("UPDATE gateways SET organization_id = :org_id"),
|
||||
{"org_id": org_id},
|
||||
)
|
||||
|
||||
user_rows = list(bind.execute(sa.text("SELECT id FROM users")))
|
||||
for row in user_rows:
|
||||
user_id = row[0]
|
||||
bind.execute(
|
||||
sa.text(
|
||||
"""
|
||||
INSERT INTO organization_members
|
||||
(id, organization_id, user_id, role, all_boards_read, all_boards_write, created_at, updated_at)
|
||||
VALUES
|
||||
(:id, :org_id, :user_id, :role, :all_read, :all_write, :now, :now)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": uuid.uuid4(),
|
||||
"org_id": org_id,
|
||||
"user_id": user_id,
|
||||
"role": "owner",
|
||||
"all_read": True,
|
||||
"all_write": True,
|
||||
"now": now,
|
||||
},
|
||||
)
|
||||
|
||||
op.alter_column("boards", "organization_id", nullable=False)
|
||||
op.alter_column("board_groups", "organization_id", nullable=False)
|
||||
op.alter_column("gateways", "organization_id", nullable=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_constraint("fk_gateways_organization_id", "gateways", type_="foreignkey")
|
||||
op.drop_constraint("fk_board_groups_organization_id", "board_groups", type_="foreignkey")
|
||||
op.drop_constraint("fk_boards_organization_id", "boards", type_="foreignkey")
|
||||
|
||||
op.drop_index("ix_gateways_organization_id", table_name="gateways")
|
||||
op.drop_index("ix_board_groups_organization_id", table_name="board_groups")
|
||||
op.drop_index("ix_boards_organization_id", table_name="boards")
|
||||
|
||||
op.drop_column("gateways", "organization_id")
|
||||
op.drop_column("board_groups", "organization_id")
|
||||
op.drop_column("boards", "organization_id")
|
||||
|
||||
op.drop_index("ix_org_invite_access_board", table_name="organization_invite_board_access")
|
||||
op.drop_index("ix_org_invite_access_invite", table_name="organization_invite_board_access")
|
||||
op.drop_table("organization_invite_board_access")
|
||||
|
||||
op.drop_index("ix_org_invites_token", table_name="organization_invites")
|
||||
op.drop_index("ix_org_invites_email", table_name="organization_invites")
|
||||
op.drop_index("ix_org_invites_org", table_name="organization_invites")
|
||||
op.drop_table("organization_invites")
|
||||
|
||||
op.drop_index("ix_org_board_access_board", table_name="organization_board_access")
|
||||
op.drop_index("ix_org_board_access_member", table_name="organization_board_access")
|
||||
op.drop_table("organization_board_access")
|
||||
|
||||
op.drop_index("ix_org_members_role", table_name="organization_members")
|
||||
op.drop_index("ix_org_members_user", table_name="organization_members")
|
||||
op.drop_index("ix_org_members_org", table_name="organization_members")
|
||||
op.drop_table("organization_members")
|
||||
|
||||
op.drop_index("ix_organizations_name", table_name="organizations")
|
||||
op.drop_table("organizations")
|
||||
@@ -1,122 +0,0 @@
|
||||
"""board group memory
|
||||
|
||||
Revision ID: 23c771c93430
|
||||
Revises: 12772fdcdfe9
|
||||
Create Date: 2026-02-07 18:00:19.065861
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "23c771c93430"
|
||||
down_revision = "12772fdcdfe9"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Repair drift: it's possible to end up with alembic_version stamped at 12772fdcdfe9
|
||||
# without actually applying the board groups schema changes. This migration makes the
|
||||
# required board_groups + boards.board_group_id objects exist before adding group memory.
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
|
||||
if not inspector.has_table("board_groups"):
|
||||
op.create_table(
|
||||
"board_groups",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("slug", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("ix_board_groups_slug", "board_groups", ["slug"], unique=False)
|
||||
else:
|
||||
indexes = {idx.get("name") for idx in inspector.get_indexes("board_groups")}
|
||||
if "ix_board_groups_slug" not in indexes:
|
||||
op.create_index("ix_board_groups_slug", "board_groups", ["slug"], unique=False)
|
||||
|
||||
inspector = sa.inspect(conn)
|
||||
board_cols = {col.get("name") for col in inspector.get_columns("boards")}
|
||||
if "board_group_id" not in board_cols:
|
||||
op.add_column("boards", sa.Column("board_group_id", sa.Uuid(), nullable=True))
|
||||
|
||||
inspector = sa.inspect(conn)
|
||||
board_indexes = {idx.get("name") for idx in inspector.get_indexes("boards")}
|
||||
if "ix_boards_board_group_id" not in board_indexes:
|
||||
op.create_index("ix_boards_board_group_id", "boards", ["board_group_id"], unique=False)
|
||||
|
||||
def _has_board_groups_fk() -> bool:
|
||||
for fk in inspector.get_foreign_keys("boards"):
|
||||
if fk.get("referred_table") != "board_groups":
|
||||
continue
|
||||
if fk.get("constrained_columns") != ["board_group_id"]:
|
||||
continue
|
||||
if fk.get("referred_columns") != ["id"]:
|
||||
continue
|
||||
return True
|
||||
return False
|
||||
|
||||
if not _has_board_groups_fk():
|
||||
op.create_foreign_key(
|
||||
"fk_boards_board_group_id_board_groups",
|
||||
"boards",
|
||||
"board_groups",
|
||||
["board_group_id"],
|
||||
["id"],
|
||||
)
|
||||
|
||||
inspector = sa.inspect(conn)
|
||||
if not inspector.has_table("board_group_memory"):
|
||||
op.create_table(
|
||||
"board_group_memory",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("board_group_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("content", sa.Text(), nullable=False),
|
||||
sa.Column("tags", sa.JSON(), nullable=True),
|
||||
sa.Column(
|
||||
"is_chat",
|
||||
sa.Boolean(),
|
||||
server_default=sa.text("false"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("source", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["board_group_id"], ["board_groups.id"]),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_board_group_memory_board_group_id",
|
||||
"board_group_memory",
|
||||
["board_group_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_board_group_memory_is_chat",
|
||||
"board_group_memory",
|
||||
["is_chat"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_board_group_memory_board_group_id_is_chat_created_at",
|
||||
"board_group_memory",
|
||||
["board_group_id", "is_chat", "created_at"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index(
|
||||
"ix_board_group_memory_board_group_id_is_chat_created_at",
|
||||
table_name="board_group_memory",
|
||||
)
|
||||
op.drop_index("ix_board_group_memory_is_chat", table_name="board_group_memory")
|
||||
op.drop_index("ix_board_group_memory_board_group_id", table_name="board_group_memory")
|
||||
op.drop_table("board_group_memory")
|
||||
@@ -1,24 +0,0 @@
|
||||
"""merge heads
|
||||
|
||||
Revision ID: 2c7b1c4d9e10
|
||||
Revises: 1f2a3b4c5d6e, af403671a8c4
|
||||
Create Date: 2026-02-07
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "2c7b1c4d9e10"
|
||||
down_revision = ("1f2a3b4c5d6e", "af403671a8c4")
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -1,80 +0,0 @@
|
||||
"""task dependencies
|
||||
|
||||
Revision ID: 3c6a2d3df4a1
|
||||
Revises: 1d844b04ee06
|
||||
Create Date: 2026-02-06
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "3c6a2d3df4a1"
|
||||
down_revision = "1d844b04ee06"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"task_dependencies",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("board_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("task_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("depends_on_task_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["board_id"],
|
||||
["boards.id"],
|
||||
ondelete="CASCADE",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["task_id"],
|
||||
["tasks.id"],
|
||||
ondelete="CASCADE",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["depends_on_task_id"],
|
||||
["tasks.id"],
|
||||
ondelete="CASCADE",
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint(
|
||||
"task_id",
|
||||
"depends_on_task_id",
|
||||
name="uq_task_dependencies_task_id_depends_on_task_id",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"task_id <> depends_on_task_id",
|
||||
name="ck_task_dependencies_no_self",
|
||||
),
|
||||
)
|
||||
|
||||
op.create_index(
|
||||
"ix_task_dependencies_board_id",
|
||||
"task_dependencies",
|
||||
["board_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_task_dependencies_task_id",
|
||||
"task_dependencies",
|
||||
["task_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_task_dependencies_depends_on_task_id",
|
||||
"task_dependencies",
|
||||
["depends_on_task_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_task_dependencies_depends_on_task_id", table_name="task_dependencies")
|
||||
op.drop_index("ix_task_dependencies_task_id", table_name="task_dependencies")
|
||||
op.drop_index("ix_task_dependencies_board_id", table_name="task_dependencies")
|
||||
op.drop_table("task_dependencies")
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
"""ensure board group memory table
|
||||
|
||||
Revision ID: 5fb3b2491090
|
||||
Revises: 23c771c93430
|
||||
Create Date: 2026-02-07 18:07:20.588662
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "5fb3b2491090"
|
||||
down_revision = "23c771c93430"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
if inspector.has_table("board_group_memory"):
|
||||
return
|
||||
|
||||
op.create_table(
|
||||
"board_group_memory",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("board_group_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("content", sa.Text(), nullable=False),
|
||||
sa.Column("tags", sa.JSON(), nullable=True),
|
||||
sa.Column(
|
||||
"is_chat",
|
||||
sa.Boolean(),
|
||||
server_default=sa.text("false"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("source", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["board_group_id"], ["board_groups.id"]),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_board_group_memory_board_group_id",
|
||||
"board_group_memory",
|
||||
["board_group_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_board_group_memory_is_chat",
|
||||
"board_group_memory",
|
||||
["is_chat"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_board_group_memory_board_group_id_is_chat_created_at",
|
||||
"board_group_memory",
|
||||
["board_group_id", "is_chat", "created_at"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# This is a repair migration. Downgrading from 5fb3b2491090 -> 23c771c93430
|
||||
# should keep the board_group_memory table (it belongs to the prior revision).
|
||||
return
|
||||
@@ -1,70 +0,0 @@
|
||||
"""add active organization to users
|
||||
|
||||
Revision ID: 6e1c9b2f7a4d
|
||||
Revises: 050c16fde00e
|
||||
Create Date: 2026-02-08
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "6e1c9b2f7a4d"
|
||||
down_revision = "050c16fde00e"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"users",
|
||||
sa.Column("active_organization_id", sa.UUID(), nullable=True),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_users_active_organization_id",
|
||||
"users",
|
||||
["active_organization_id"],
|
||||
)
|
||||
op.create_foreign_key(
|
||||
"fk_users_active_organization",
|
||||
"users",
|
||||
"organizations",
|
||||
["active_organization_id"],
|
||||
["id"],
|
||||
)
|
||||
|
||||
bind = op.get_bind()
|
||||
rows = bind.execute(
|
||||
sa.text(
|
||||
"""
|
||||
SELECT user_id, organization_id
|
||||
FROM organization_members
|
||||
ORDER BY user_id, created_at ASC
|
||||
"""
|
||||
)
|
||||
).fetchall()
|
||||
seen: set[str] = set()
|
||||
for row in rows:
|
||||
user_id = str(row.user_id)
|
||||
if user_id in seen:
|
||||
continue
|
||||
seen.add(user_id)
|
||||
bind.execute(
|
||||
sa.text(
|
||||
"""
|
||||
UPDATE users
|
||||
SET active_organization_id = :org_id
|
||||
WHERE id = :user_id
|
||||
AND active_organization_id IS NULL
|
||||
"""
|
||||
),
|
||||
{"org_id": row.organization_id, "user_id": row.user_id},
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_constraint("fk_users_active_organization", "users", type_="foreignkey")
|
||||
op.drop_index("ix_users_active_organization_id", table_name="users")
|
||||
op.drop_column("users", "active_organization_id")
|
||||
@@ -1,34 +0,0 @@
|
||||
"""remove skyll_enabled
|
||||
|
||||
Revision ID: 9f0c4fb2a7b8
|
||||
Revises: 3c6a2d3df4a1
|
||||
Create Date: 2026-02-06
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "9f0c4fb2a7b8"
|
||||
down_revision = "3c6a2d3df4a1"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.drop_column("gateways", "skyll_enabled")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.add_column(
|
||||
"gateways",
|
||||
sa.Column(
|
||||
"skyll_enabled",
|
||||
sa.Boolean(),
|
||||
server_default=sa.text("false"),
|
||||
nullable=False,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
"""repair board groups schema
|
||||
|
||||
Revision ID: af403671a8c4
|
||||
Revises: 5fb3b2491090
|
||||
Create Date: 2026-02-07
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "af403671a8c4"
|
||||
down_revision = "5fb3b2491090"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Repair drift: it is possible to end up with alembic_version stamped at (or beyond)
|
||||
# the board group revisions without having the underlying DB objects present.
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
|
||||
if not inspector.has_table("board_groups"):
|
||||
op.create_table(
|
||||
"board_groups",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("slug", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("ix_board_groups_slug", "board_groups", ["slug"], unique=False)
|
||||
else:
|
||||
indexes = {idx.get("name") for idx in inspector.get_indexes("board_groups")}
|
||||
if "ix_board_groups_slug" not in indexes:
|
||||
op.create_index("ix_board_groups_slug", "board_groups", ["slug"], unique=False)
|
||||
|
||||
inspector = sa.inspect(conn)
|
||||
board_cols = {col.get("name") for col in inspector.get_columns("boards")}
|
||||
if "board_group_id" not in board_cols:
|
||||
op.add_column("boards", sa.Column("board_group_id", sa.Uuid(), nullable=True))
|
||||
|
||||
inspector = sa.inspect(conn)
|
||||
board_indexes = {idx.get("name") for idx in inspector.get_indexes("boards")}
|
||||
if "ix_boards_board_group_id" not in board_indexes:
|
||||
op.create_index("ix_boards_board_group_id", "boards", ["board_group_id"], unique=False)
|
||||
|
||||
def _has_board_groups_fk() -> bool:
|
||||
for fk in inspector.get_foreign_keys("boards"):
|
||||
if fk.get("referred_table") != "board_groups":
|
||||
continue
|
||||
if fk.get("constrained_columns") != ["board_group_id"]:
|
||||
continue
|
||||
if fk.get("referred_columns") != ["id"]:
|
||||
continue
|
||||
return True
|
||||
return False
|
||||
|
||||
if not _has_board_groups_fk():
|
||||
op.create_foreign_key(
|
||||
"fk_boards_board_group_id_board_groups",
|
||||
"boards",
|
||||
"board_groups",
|
||||
["board_group_id"],
|
||||
["id"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Repair migration: do not attempt to undo drift fixes automatically.
|
||||
return
|
||||
|
||||
@@ -21,6 +21,7 @@ from app.core.auth import AuthContext, get_auth_context
|
||||
from app.core.time import utcnow
|
||||
from app.db.pagination import paginate
|
||||
from app.db.session import async_session_maker, get_session
|
||||
from app.db.sqlmodel_exec import exec_dml
|
||||
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
|
||||
from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message
|
||||
from app.models.activity_events import ActivityEvent
|
||||
@@ -973,7 +974,8 @@ async def delete_agent(
|
||||
agent_id=None,
|
||||
)
|
||||
now = datetime.now()
|
||||
await session.execute(
|
||||
await exec_dml(
|
||||
session,
|
||||
update(Task)
|
||||
.where(col(Task.assigned_agent_id) == agent.id)
|
||||
.where(col(Task.status) == "in_progress")
|
||||
@@ -982,19 +984,21 @@ async def delete_agent(
|
||||
status="inbox",
|
||||
in_progress_at=None,
|
||||
updated_at=now,
|
||||
),
|
||||
)
|
||||
)
|
||||
await session.execute(
|
||||
await exec_dml(
|
||||
session,
|
||||
update(Task)
|
||||
.where(col(Task.assigned_agent_id) == agent.id)
|
||||
.where(col(Task.status) != "in_progress")
|
||||
.values(
|
||||
assigned_agent_id=None,
|
||||
updated_at=now,
|
||||
),
|
||||
)
|
||||
)
|
||||
await session.execute(
|
||||
update(ActivityEvent).where(col(ActivityEvent.agent_id) == agent.id).values(agent_id=None)
|
||||
await exec_dml(
|
||||
session,
|
||||
update(ActivityEvent).where(col(ActivityEvent.agent_id) == agent.id).values(agent_id=None),
|
||||
)
|
||||
await session.delete(agent)
|
||||
await session.commit()
|
||||
|
||||
@@ -14,6 +14,7 @@ from app.core.time import utcnow
|
||||
from app.db import crud
|
||||
from app.db.pagination import paginate
|
||||
from app.db.session import get_session
|
||||
from app.db.sqlmodel_exec import exec_dml
|
||||
from app.models.agents import Agent
|
||||
from app.models.board_group_memory import BoardGroupMemory
|
||||
from app.models.board_groups import BoardGroup
|
||||
@@ -262,10 +263,8 @@ async def update_board_group(
|
||||
updates = payload.model_dump(exclude_unset=True)
|
||||
if "slug" in updates and updates["slug"] is not None and not updates["slug"].strip():
|
||||
updates["slug"] = _slugify(updates.get("name") or group.name)
|
||||
for key, value in updates.items():
|
||||
setattr(group, key, value)
|
||||
group.updated_at = utcnow()
|
||||
return await crud.save(session, group)
|
||||
updates["updated_at"] = utcnow()
|
||||
return await crud.patch(session, group, updates)
|
||||
|
||||
|
||||
@router.delete("/{group_id}", response_model=OkResponse)
|
||||
@@ -277,12 +276,14 @@ async def delete_board_group(
|
||||
await _require_group_access(session, group_id=group_id, member=ctx.member, write=True)
|
||||
|
||||
# Boards reference groups, so clear the FK first to keep deletes simple.
|
||||
await session.execute(
|
||||
update(Board).where(col(Board.board_group_id) == group_id).values(board_group_id=None)
|
||||
await exec_dml(
|
||||
session,
|
||||
update(Board).where(col(Board.board_group_id) == group_id).values(board_group_id=None),
|
||||
)
|
||||
await session.execute(
|
||||
delete(BoardGroupMemory).where(col(BoardGroupMemory.board_group_id) == group_id)
|
||||
await exec_dml(
|
||||
session,
|
||||
delete(BoardGroupMemory).where(col(BoardGroupMemory.board_group_id) == group_id),
|
||||
)
|
||||
await session.execute(delete(BoardGroup).where(col(BoardGroup.id) == group_id))
|
||||
await exec_dml(session, delete(BoardGroup).where(col(BoardGroup.id) == group_id))
|
||||
await session.commit()
|
||||
return OkResponse()
|
||||
|
||||
@@ -19,6 +19,7 @@ from app.core.time import utcnow
|
||||
from app.db import crud
|
||||
from app.db.pagination import paginate
|
||||
from app.db.session import get_session
|
||||
from app.db.sqlmodel_exec import exec_dml
|
||||
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
|
||||
from app.integrations.openclaw_gateway import (
|
||||
OpenClawGatewayError,
|
||||
@@ -140,8 +141,7 @@ async def _apply_board_update(
|
||||
updates["board_group_id"],
|
||||
organization_id=board.organization_id,
|
||||
)
|
||||
for key, value in updates.items():
|
||||
setattr(board, key, value)
|
||||
crud.apply_updates(board, updates)
|
||||
if updates.get("board_type") == "goal":
|
||||
# Validate only when explicitly switching to goal boards.
|
||||
if not board.objective or not board.success_metrics:
|
||||
@@ -307,36 +307,43 @@ async def delete_board(
|
||||
) from exc
|
||||
|
||||
if task_ids:
|
||||
await session.execute(delete(ActivityEvent).where(col(ActivityEvent.task_id).in_(task_ids)))
|
||||
await session.execute(delete(TaskDependency).where(col(TaskDependency.board_id) == board.id))
|
||||
await session.execute(delete(TaskFingerprint).where(col(TaskFingerprint.board_id) == board.id))
|
||||
await exec_dml(
|
||||
session, delete(ActivityEvent).where(col(ActivityEvent.task_id).in_(task_ids))
|
||||
)
|
||||
await exec_dml(session, delete(TaskDependency).where(col(TaskDependency.board_id) == board.id))
|
||||
await exec_dml(
|
||||
session, delete(TaskFingerprint).where(col(TaskFingerprint.board_id) == board.id)
|
||||
)
|
||||
|
||||
# Approvals can reference tasks and agents, so delete before both.
|
||||
await session.execute(delete(Approval).where(col(Approval.board_id) == board.id))
|
||||
await exec_dml(session, delete(Approval).where(col(Approval.board_id) == board.id))
|
||||
|
||||
await session.execute(delete(BoardMemory).where(col(BoardMemory.board_id) == board.id))
|
||||
await session.execute(
|
||||
delete(BoardOnboardingSession).where(col(BoardOnboardingSession.board_id) == board.id)
|
||||
await exec_dml(session, delete(BoardMemory).where(col(BoardMemory.board_id) == board.id))
|
||||
await exec_dml(
|
||||
session,
|
||||
delete(BoardOnboardingSession).where(col(BoardOnboardingSession.board_id) == board.id),
|
||||
)
|
||||
await session.execute(
|
||||
delete(OrganizationBoardAccess).where(col(OrganizationBoardAccess.board_id) == board.id)
|
||||
await exec_dml(
|
||||
session,
|
||||
delete(OrganizationBoardAccess).where(col(OrganizationBoardAccess.board_id) == board.id),
|
||||
)
|
||||
await session.execute(
|
||||
await exec_dml(
|
||||
session,
|
||||
delete(OrganizationInviteBoardAccess).where(
|
||||
col(OrganizationInviteBoardAccess.board_id) == board.id
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
# Tasks reference agents (assigned_agent_id) and have dependents (fingerprints/dependencies), so
|
||||
# delete tasks before agents.
|
||||
await session.execute(delete(Task).where(col(Task.board_id) == board.id))
|
||||
await exec_dml(session, delete(Task).where(col(Task.board_id) == board.id))
|
||||
|
||||
if agents:
|
||||
agent_ids = [agent.id for agent in agents]
|
||||
await session.execute(
|
||||
delete(ActivityEvent).where(col(ActivityEvent.agent_id).in_(agent_ids))
|
||||
await exec_dml(
|
||||
session, delete(ActivityEvent).where(col(ActivityEvent.agent_id).in_(agent_ids))
|
||||
)
|
||||
await session.execute(delete(Agent).where(col(Agent.id).in_(agent_ids)))
|
||||
await exec_dml(session, delete(Agent).where(col(Agent.id).in_(agent_ids)))
|
||||
await session.delete(board)
|
||||
await session.commit()
|
||||
return OkResponse()
|
||||
|
||||
@@ -2,14 +2,16 @@ from __future__ import annotations
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlmodel import col, select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.api.deps import require_org_admin
|
||||
from app.api.queryset import api_qs
|
||||
from app.core.agent_tokens import generate_agent_token, hash_agent_token
|
||||
from app.core.auth import AuthContext, get_auth_context
|
||||
from app.core.time import utcnow
|
||||
from app.db import crud
|
||||
from app.db.pagination import paginate
|
||||
from app.db.session import get_session
|
||||
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
|
||||
@@ -35,6 +37,22 @@ def _main_agent_name(gateway: Gateway) -> str:
|
||||
return f"{gateway.name} Main"
|
||||
|
||||
|
||||
async def _require_gateway(
|
||||
session: AsyncSession,
|
||||
*,
|
||||
gateway_id: UUID,
|
||||
organization_id: UUID,
|
||||
) -> Gateway:
|
||||
return await (
|
||||
api_qs(Gateway)
|
||||
.filter(
|
||||
col(Gateway.id) == gateway_id,
|
||||
col(Gateway.organization_id) == organization_id,
|
||||
)
|
||||
.first_or_404(session, detail="Gateway not found")
|
||||
)
|
||||
|
||||
|
||||
async def _find_main_agent(
|
||||
session: AsyncSession,
|
||||
gateway: Gateway,
|
||||
@@ -135,9 +153,10 @@ async def list_gateways(
|
||||
ctx: OrganizationContext = Depends(require_org_admin),
|
||||
) -> DefaultLimitOffsetPage[GatewayRead]:
|
||||
statement = (
|
||||
select(Gateway)
|
||||
.where(col(Gateway.organization_id) == ctx.organization.id)
|
||||
api_qs(Gateway)
|
||||
.filter(col(Gateway.organization_id) == ctx.organization.id)
|
||||
.order_by(col(Gateway.created_at).desc())
|
||||
.statement
|
||||
)
|
||||
return await paginate(session, statement)
|
||||
|
||||
@@ -151,10 +170,7 @@ async def create_gateway(
|
||||
) -> Gateway:
|
||||
data = payload.model_dump()
|
||||
data["organization_id"] = ctx.organization.id
|
||||
gateway = Gateway.model_validate(data)
|
||||
session.add(gateway)
|
||||
await session.commit()
|
||||
await session.refresh(gateway)
|
||||
gateway = await crud.create(session, Gateway, **data)
|
||||
await _ensure_main_agent(session, gateway, auth, action="provision")
|
||||
return gateway
|
||||
|
||||
@@ -165,10 +181,11 @@ async def get_gateway(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
ctx: OrganizationContext = Depends(require_org_admin),
|
||||
) -> Gateway:
|
||||
gateway = await session.get(Gateway, gateway_id)
|
||||
if gateway is None or gateway.organization_id != ctx.organization.id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Gateway not found")
|
||||
return gateway
|
||||
return await _require_gateway(
|
||||
session,
|
||||
gateway_id=gateway_id,
|
||||
organization_id=ctx.organization.id,
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/{gateway_id}", response_model=GatewayRead)
|
||||
@@ -179,17 +196,15 @@ async def update_gateway(
|
||||
auth: AuthContext = Depends(get_auth_context),
|
||||
ctx: OrganizationContext = Depends(require_org_admin),
|
||||
) -> Gateway:
|
||||
gateway = await session.get(Gateway, gateway_id)
|
||||
if gateway is None or gateway.organization_id != ctx.organization.id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Gateway not found")
|
||||
gateway = await _require_gateway(
|
||||
session,
|
||||
gateway_id=gateway_id,
|
||||
organization_id=ctx.organization.id,
|
||||
)
|
||||
previous_name = gateway.name
|
||||
previous_session_key = gateway.main_session_key
|
||||
updates = payload.model_dump(exclude_unset=True)
|
||||
for key, value in updates.items():
|
||||
setattr(gateway, key, value)
|
||||
session.add(gateway)
|
||||
await session.commit()
|
||||
await session.refresh(gateway)
|
||||
await crud.patch(session, gateway, updates)
|
||||
await _ensure_main_agent(
|
||||
session,
|
||||
gateway,
|
||||
@@ -213,9 +228,11 @@ async def sync_gateway_templates(
|
||||
auth: AuthContext = Depends(get_auth_context),
|
||||
ctx: OrganizationContext = Depends(require_org_admin),
|
||||
) -> GatewayTemplatesSyncResult:
|
||||
gateway = await session.get(Gateway, gateway_id)
|
||||
if gateway is None or gateway.organization_id != ctx.organization.id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Gateway not found")
|
||||
gateway = await _require_gateway(
|
||||
session,
|
||||
gateway_id=gateway_id,
|
||||
organization_id=ctx.organization.id,
|
||||
)
|
||||
return await sync_gateway_templates_service(
|
||||
session,
|
||||
gateway,
|
||||
@@ -234,9 +251,10 @@ async def delete_gateway(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
ctx: OrganizationContext = Depends(require_org_admin),
|
||||
) -> OkResponse:
|
||||
gateway = await session.get(Gateway, gateway_id)
|
||||
if gateway is None or gateway.organization_id != ctx.organization.id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Gateway not found")
|
||||
await session.delete(gateway)
|
||||
await session.commit()
|
||||
gateway = await _require_gateway(
|
||||
session,
|
||||
gateway_id=gateway_id,
|
||||
organization_id=ctx.organization.id,
|
||||
)
|
||||
await crud.delete(session, gateway)
|
||||
return OkResponse()
|
||||
|
||||
@@ -10,10 +10,13 @@ from sqlmodel import col, select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.api.deps import require_org_admin, require_org_member
|
||||
from app.api.queryset import api_qs
|
||||
from app.core.auth import AuthContext, get_auth_context
|
||||
from app.core.time import utcnow
|
||||
from app.db import crud
|
||||
from app.db.pagination import paginate
|
||||
from app.db.session import get_session
|
||||
from app.db.sqlmodel_exec import exec_dml
|
||||
from app.models.activity_events import ActivityEvent
|
||||
from app.models.agents import Agent
|
||||
from app.models.approvals import Approval
|
||||
@@ -72,6 +75,38 @@ def _member_to_read(member: OrganizationMember, user: User | None) -> Organizati
|
||||
return model
|
||||
|
||||
|
||||
async def _require_org_member(
|
||||
session: AsyncSession,
|
||||
*,
|
||||
organization_id: UUID,
|
||||
member_id: UUID,
|
||||
) -> OrganizationMember:
|
||||
return await (
|
||||
api_qs(OrganizationMember)
|
||||
.filter(
|
||||
col(OrganizationMember.id) == member_id,
|
||||
col(OrganizationMember.organization_id) == organization_id,
|
||||
)
|
||||
.first_or_404(session)
|
||||
)
|
||||
|
||||
|
||||
async def _require_org_invite(
|
||||
session: AsyncSession,
|
||||
*,
|
||||
organization_id: UUID,
|
||||
invite_id: UUID,
|
||||
) -> OrganizationInvite:
|
||||
return await (
|
||||
api_qs(OrganizationInvite)
|
||||
.filter(
|
||||
col(OrganizationInvite.id) == invite_id,
|
||||
col(OrganizationInvite.organization_id) == organization_id,
|
||||
)
|
||||
.first_or_404(session)
|
||||
)
|
||||
|
||||
|
||||
@router.post("", response_model=OrganizationRead)
|
||||
async def create_organization(
|
||||
payload: OrganizationCreate,
|
||||
@@ -188,55 +223,67 @@ async def delete_my_org(
|
||||
)
|
||||
group_ids = select(BoardGroup.id).where(col(BoardGroup.organization_id) == org_id)
|
||||
|
||||
await session.execute(delete(ActivityEvent).where(col(ActivityEvent.task_id).in_(task_ids)))
|
||||
await session.execute(delete(ActivityEvent).where(col(ActivityEvent.agent_id).in_(agent_ids)))
|
||||
await session.execute(delete(TaskDependency).where(col(TaskDependency.board_id).in_(board_ids)))
|
||||
await session.execute(
|
||||
delete(TaskFingerprint).where(col(TaskFingerprint.board_id).in_(board_ids))
|
||||
await exec_dml(session, delete(ActivityEvent).where(col(ActivityEvent.task_id).in_(task_ids)))
|
||||
await exec_dml(session, delete(ActivityEvent).where(col(ActivityEvent.agent_id).in_(agent_ids)))
|
||||
await exec_dml(
|
||||
session, delete(TaskDependency).where(col(TaskDependency.board_id).in_(board_ids))
|
||||
)
|
||||
await session.execute(delete(Approval).where(col(Approval.board_id).in_(board_ids)))
|
||||
await session.execute(delete(BoardMemory).where(col(BoardMemory.board_id).in_(board_ids)))
|
||||
await session.execute(
|
||||
delete(BoardOnboardingSession).where(col(BoardOnboardingSession.board_id).in_(board_ids))
|
||||
await exec_dml(
|
||||
session,
|
||||
delete(TaskFingerprint).where(col(TaskFingerprint.board_id).in_(board_ids)),
|
||||
)
|
||||
await session.execute(
|
||||
delete(OrganizationBoardAccess).where(col(OrganizationBoardAccess.board_id).in_(board_ids))
|
||||
await exec_dml(session, delete(Approval).where(col(Approval.board_id).in_(board_ids)))
|
||||
await exec_dml(session, delete(BoardMemory).where(col(BoardMemory.board_id).in_(board_ids)))
|
||||
await exec_dml(
|
||||
session,
|
||||
delete(BoardOnboardingSession).where(col(BoardOnboardingSession.board_id).in_(board_ids)),
|
||||
)
|
||||
await session.execute(
|
||||
await exec_dml(
|
||||
session,
|
||||
delete(OrganizationBoardAccess).where(col(OrganizationBoardAccess.board_id).in_(board_ids)),
|
||||
)
|
||||
await exec_dml(
|
||||
session,
|
||||
delete(OrganizationInviteBoardAccess).where(
|
||||
col(OrganizationInviteBoardAccess.board_id).in_(board_ids)
|
||||
),
|
||||
)
|
||||
)
|
||||
await session.execute(
|
||||
await exec_dml(
|
||||
session,
|
||||
delete(OrganizationBoardAccess).where(
|
||||
col(OrganizationBoardAccess.organization_member_id).in_(member_ids)
|
||||
),
|
||||
)
|
||||
)
|
||||
await session.execute(
|
||||
await exec_dml(
|
||||
session,
|
||||
delete(OrganizationInviteBoardAccess).where(
|
||||
col(OrganizationInviteBoardAccess.organization_invite_id).in_(invite_ids)
|
||||
),
|
||||
)
|
||||
await exec_dml(session, delete(Task).where(col(Task.board_id).in_(board_ids)))
|
||||
await exec_dml(session, delete(Agent).where(col(Agent.board_id).in_(board_ids)))
|
||||
await exec_dml(session, delete(Board).where(col(Board.organization_id) == org_id))
|
||||
await exec_dml(
|
||||
session,
|
||||
delete(BoardGroupMemory).where(col(BoardGroupMemory.board_group_id).in_(group_ids)),
|
||||
)
|
||||
await session.execute(delete(Task).where(col(Task.board_id).in_(board_ids)))
|
||||
await session.execute(delete(Agent).where(col(Agent.board_id).in_(board_ids)))
|
||||
await session.execute(delete(Board).where(col(Board.organization_id) == org_id))
|
||||
await session.execute(
|
||||
delete(BoardGroupMemory).where(col(BoardGroupMemory.board_group_id).in_(group_ids))
|
||||
await exec_dml(session, delete(BoardGroup).where(col(BoardGroup.organization_id) == org_id))
|
||||
await exec_dml(session, delete(Gateway).where(col(Gateway.organization_id) == org_id))
|
||||
await exec_dml(
|
||||
session,
|
||||
delete(OrganizationInvite).where(col(OrganizationInvite.organization_id) == org_id),
|
||||
)
|
||||
await session.execute(delete(BoardGroup).where(col(BoardGroup.organization_id) == org_id))
|
||||
await session.execute(delete(Gateway).where(col(Gateway.organization_id) == org_id))
|
||||
await session.execute(
|
||||
delete(OrganizationInvite).where(col(OrganizationInvite.organization_id) == org_id)
|
||||
await exec_dml(
|
||||
session,
|
||||
delete(OrganizationMember).where(col(OrganizationMember.organization_id) == org_id),
|
||||
)
|
||||
await session.execute(
|
||||
delete(OrganizationMember).where(col(OrganizationMember.organization_id) == org_id)
|
||||
)
|
||||
await session.execute(
|
||||
await exec_dml(
|
||||
session,
|
||||
update(User)
|
||||
.where(col(User.active_organization_id) == org_id)
|
||||
.values(active_organization_id=None)
|
||||
.values(active_organization_id=None),
|
||||
)
|
||||
await session.execute(delete(Organization).where(col(Organization.id) == org_id))
|
||||
await exec_dml(session, delete(Organization).where(col(Organization.id) == org_id))
|
||||
await session.commit()
|
||||
return OkResponse()
|
||||
|
||||
@@ -288,9 +335,11 @@ async def get_org_member(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
ctx: OrganizationContext = Depends(require_org_member),
|
||||
) -> OrganizationMemberRead:
|
||||
member = await session.get(OrganizationMember, member_id)
|
||||
if member is None or member.organization_id != ctx.organization.id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
member = await _require_org_member(
|
||||
session,
|
||||
organization_id=ctx.organization.id,
|
||||
member_id=member_id,
|
||||
)
|
||||
if not is_org_admin(ctx.member) and member.user_id != ctx.member.user_id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
user = await session.get(User, member.user_id)
|
||||
@@ -315,16 +364,16 @@ async def update_org_member(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
ctx: OrganizationContext = Depends(require_org_admin),
|
||||
) -> OrganizationMemberRead:
|
||||
member = await session.get(OrganizationMember, member_id)
|
||||
if member is None or member.organization_id != ctx.organization.id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
member = await _require_org_member(
|
||||
session,
|
||||
organization_id=ctx.organization.id,
|
||||
member_id=member_id,
|
||||
)
|
||||
updates = payload.model_dump(exclude_unset=True)
|
||||
if "role" in updates and updates["role"] is not None:
|
||||
member.role = normalize_role(updates["role"])
|
||||
member.updated_at = utcnow()
|
||||
session.add(member)
|
||||
await session.commit()
|
||||
await session.refresh(member)
|
||||
updates["role"] = normalize_role(updates["role"])
|
||||
updates["updated_at"] = utcnow()
|
||||
member = await crud.patch(session, member, updates)
|
||||
user = await session.get(User, member.user_id)
|
||||
return _member_to_read(member, user)
|
||||
|
||||
@@ -336,9 +385,11 @@ async def update_member_access(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
ctx: OrganizationContext = Depends(require_org_admin),
|
||||
) -> OrganizationMemberRead:
|
||||
member = await session.get(OrganizationMember, member_id)
|
||||
if member is None or member.organization_id != ctx.organization.id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
member = await _require_org_member(
|
||||
session,
|
||||
organization_id=ctx.organization.id,
|
||||
member_id=member_id,
|
||||
)
|
||||
|
||||
board_ids = {entry.board_id for entry in payload.board_access}
|
||||
if board_ids:
|
||||
@@ -393,10 +444,11 @@ async def remove_org_member(
|
||||
detail="Organization must have at least one owner",
|
||||
)
|
||||
|
||||
await session.execute(
|
||||
await exec_dml(
|
||||
session,
|
||||
delete(OrganizationBoardAccess).where(
|
||||
col(OrganizationBoardAccess.organization_member_id) == member.id
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
user = await session.get(User, member.user_id)
|
||||
@@ -412,8 +464,7 @@ async def remove_org_member(
|
||||
user.active_organization_id = fallback_org_id
|
||||
session.add(user)
|
||||
|
||||
await session.delete(member)
|
||||
await session.commit()
|
||||
await crud.delete(session, member)
|
||||
return OkResponse()
|
||||
|
||||
|
||||
@@ -423,10 +474,11 @@ async def list_org_invites(
|
||||
ctx: OrganizationContext = Depends(require_org_admin),
|
||||
) -> DefaultLimitOffsetPage[OrganizationInviteRead]:
|
||||
statement = (
|
||||
select(OrganizationInvite)
|
||||
.where(col(OrganizationInvite.organization_id) == ctx.organization.id)
|
||||
.where(col(OrganizationInvite.accepted_at).is_(None))
|
||||
api_qs(OrganizationInvite)
|
||||
.filter(col(OrganizationInvite.organization_id) == ctx.organization.id)
|
||||
.filter(col(OrganizationInvite.accepted_at).is_(None))
|
||||
.order_by(col(OrganizationInvite.created_at).desc())
|
||||
.statement
|
||||
)
|
||||
return await paginate(session, statement)
|
||||
|
||||
@@ -491,16 +543,18 @@ async def revoke_org_invite(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
ctx: OrganizationContext = Depends(require_org_admin),
|
||||
) -> OrganizationInviteRead:
|
||||
invite = await session.get(OrganizationInvite, invite_id)
|
||||
if invite is None or invite.organization_id != ctx.organization.id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
await session.execute(
|
||||
invite = await _require_org_invite(
|
||||
session,
|
||||
organization_id=ctx.organization.id,
|
||||
invite_id=invite_id,
|
||||
)
|
||||
await exec_dml(
|
||||
session,
|
||||
delete(OrganizationInviteBoardAccess).where(
|
||||
col(OrganizationInviteBoardAccess.organization_invite_id) == invite.id
|
||||
),
|
||||
)
|
||||
await session.delete(invite)
|
||||
await session.commit()
|
||||
await crud.delete(session, invite)
|
||||
return OrganizationInviteRead.model_validate(invite, from_attributes=True)
|
||||
|
||||
|
||||
|
||||
56
backend/app/api/queryset.py
Normal file
56
backend/app/api/queryset.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from sqlmodel.sql.expression import SelectOfScalar
|
||||
|
||||
from app.db.queryset import QuerySet, qs
|
||||
|
||||
ModelT = TypeVar("ModelT")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class APIQuerySet(Generic[ModelT]):
|
||||
queryset: QuerySet[ModelT]
|
||||
|
||||
@property
|
||||
def statement(self) -> SelectOfScalar[ModelT]:
|
||||
return self.queryset.statement
|
||||
|
||||
def filter(self, *criteria: Any) -> APIQuerySet[ModelT]:
|
||||
return APIQuerySet(self.queryset.filter(*criteria))
|
||||
|
||||
def order_by(self, *ordering: Any) -> APIQuerySet[ModelT]:
|
||||
return APIQuerySet(self.queryset.order_by(*ordering))
|
||||
|
||||
def limit(self, value: int) -> APIQuerySet[ModelT]:
|
||||
return APIQuerySet(self.queryset.limit(value))
|
||||
|
||||
def offset(self, value: int) -> APIQuerySet[ModelT]:
|
||||
return APIQuerySet(self.queryset.offset(value))
|
||||
|
||||
async def all(self, session: AsyncSession) -> list[ModelT]:
|
||||
return await self.queryset.all(session)
|
||||
|
||||
async def first(self, session: AsyncSession) -> ModelT | None:
|
||||
return await self.queryset.first(session)
|
||||
|
||||
async def first_or_404(
|
||||
self,
|
||||
session: AsyncSession,
|
||||
*,
|
||||
detail: str | None = None,
|
||||
) -> ModelT:
|
||||
obj = await self.first(session)
|
||||
if obj is not None:
|
||||
return obj
|
||||
if detail is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=detail)
|
||||
|
||||
|
||||
def api_qs(model: type[ModelT]) -> APIQuerySet[ModelT]:
|
||||
return APIQuerySet(qs(model))
|
||||
@@ -27,6 +27,7 @@ from app.core.auth import AuthContext
|
||||
from app.core.time import utcnow
|
||||
from app.db.pagination import paginate
|
||||
from app.db.session import async_session_maker, get_session
|
||||
from app.db.sqlmodel_exec import exec_dml
|
||||
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
|
||||
from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message
|
||||
from app.models.activity_events import ActivityEvent
|
||||
@@ -990,16 +991,17 @@ async def delete_task(
|
||||
if auth.user is None:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
await require_board_access(session, user=auth.user, board=board, write=True)
|
||||
await session.execute(delete(ActivityEvent).where(col(ActivityEvent.task_id) == task.id))
|
||||
await session.execute(delete(TaskFingerprint).where(col(TaskFingerprint.task_id) == task.id))
|
||||
await session.execute(delete(Approval).where(col(Approval.task_id) == task.id))
|
||||
await session.execute(
|
||||
await exec_dml(session, delete(ActivityEvent).where(col(ActivityEvent.task_id) == task.id))
|
||||
await exec_dml(session, delete(TaskFingerprint).where(col(TaskFingerprint.task_id) == task.id))
|
||||
await exec_dml(session, delete(Approval).where(col(Approval.task_id) == task.id))
|
||||
await exec_dml(
|
||||
session,
|
||||
delete(TaskDependency).where(
|
||||
or_(
|
||||
col(TaskDependency.task_id) == task.id,
|
||||
col(TaskDependency.depends_on_task_id) == task.id,
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
await session.delete(task)
|
||||
await session.commit()
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Iterable, Mapping
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlmodel import SQLModel, select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from sqlmodel.sql.expression import SelectOfScalar
|
||||
|
||||
ModelT = TypeVar("ModelT", bound=SQLModel)
|
||||
|
||||
@@ -18,15 +19,19 @@ class MultipleObjectsReturned(LookupError):
|
||||
pass
|
||||
|
||||
|
||||
def _lookup_statement(model: type[ModelT], lookup: Mapping[str, Any]) -> SelectOfScalar[ModelT]:
|
||||
stmt = select(model)
|
||||
for key, value in lookup.items():
|
||||
stmt = stmt.where(getattr(model, key) == value)
|
||||
return stmt
|
||||
|
||||
|
||||
async def get_by_id(session: AsyncSession, model: type[ModelT], obj_id: Any) -> ModelT | None:
|
||||
return await session.get(model, obj_id)
|
||||
|
||||
|
||||
async def get(session: AsyncSession, model: type[ModelT], **lookup: Any) -> ModelT:
|
||||
stmt = select(model)
|
||||
for key, value in lookup.items():
|
||||
stmt = stmt.where(getattr(model, key) == value)
|
||||
stmt = stmt.limit(2)
|
||||
stmt = _lookup_statement(model, lookup).limit(2)
|
||||
items = (await session.exec(stmt)).all()
|
||||
if not items:
|
||||
raise DoesNotExist(f"{model.__name__} matching query does not exist.")
|
||||
@@ -38,9 +43,7 @@ async def get(session: AsyncSession, model: type[ModelT], **lookup: Any) -> Mode
|
||||
|
||||
|
||||
async def get_one_by(session: AsyncSession, model: type[ModelT], **lookup: Any) -> ModelT | None:
|
||||
stmt = select(model)
|
||||
for key, value in lookup.items():
|
||||
stmt = stmt.where(getattr(model, key) == value)
|
||||
stmt = _lookup_statement(model, lookup)
|
||||
return (await session.exec(stmt)).first()
|
||||
|
||||
|
||||
@@ -84,6 +87,64 @@ async def delete(session: AsyncSession, obj: ModelT, *, commit: bool = True) ->
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def list_by(
|
||||
session: AsyncSession,
|
||||
model: type[ModelT],
|
||||
*,
|
||||
order_by: Iterable[Any] = (),
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
**lookup: Any,
|
||||
) -> list[ModelT]:
|
||||
stmt = _lookup_statement(model, lookup)
|
||||
for ordering in order_by:
|
||||
stmt = stmt.order_by(ordering)
|
||||
if offset is not None:
|
||||
stmt = stmt.offset(offset)
|
||||
if limit is not None:
|
||||
stmt = stmt.limit(limit)
|
||||
return list(await session.exec(stmt))
|
||||
|
||||
|
||||
async def exists(session: AsyncSession, model: type[ModelT], **lookup: Any) -> bool:
|
||||
return (await session.exec(_lookup_statement(model, lookup).limit(1))).first() is not None
|
||||
|
||||
|
||||
def apply_updates(
|
||||
obj: ModelT,
|
||||
updates: Mapping[str, Any],
|
||||
*,
|
||||
exclude_none: bool = False,
|
||||
allowed_fields: set[str] | None = None,
|
||||
) -> ModelT:
|
||||
for key, value in updates.items():
|
||||
if allowed_fields is not None and key not in allowed_fields:
|
||||
continue
|
||||
if exclude_none and value is None:
|
||||
continue
|
||||
setattr(obj, key, value)
|
||||
return obj
|
||||
|
||||
|
||||
async def patch(
|
||||
session: AsyncSession,
|
||||
obj: ModelT,
|
||||
updates: Mapping[str, Any],
|
||||
*,
|
||||
exclude_none: bool = False,
|
||||
allowed_fields: set[str] | None = None,
|
||||
commit: bool = True,
|
||||
refresh: bool = True,
|
||||
) -> ModelT:
|
||||
apply_updates(
|
||||
obj,
|
||||
updates,
|
||||
exclude_none=exclude_none,
|
||||
allowed_fields=allowed_fields,
|
||||
)
|
||||
return await save(session, obj, commit=commit, refresh=refresh)
|
||||
|
||||
|
||||
async def get_or_create(
|
||||
session: AsyncSession,
|
||||
model: type[ModelT],
|
||||
@@ -93,9 +154,7 @@ async def get_or_create(
|
||||
refresh: bool = True,
|
||||
**lookup: Any,
|
||||
) -> tuple[ModelT, bool]:
|
||||
stmt = select(model)
|
||||
for key, value in lookup.items():
|
||||
stmt = stmt.where(getattr(model, key) == value)
|
||||
stmt = _lookup_statement(model, lookup)
|
||||
|
||||
existing = (await session.exec(stmt)).first()
|
||||
if existing is not None:
|
||||
|
||||
43
backend/app/db/queryset.py
Normal file
43
backend/app/db/queryset.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
from sqlmodel import select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from sqlmodel.sql.expression import SelectOfScalar
|
||||
|
||||
ModelT = TypeVar("ModelT")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class QuerySet(Generic[ModelT]):
|
||||
statement: SelectOfScalar[ModelT]
|
||||
|
||||
def filter(self, *criteria: Any) -> QuerySet[ModelT]:
|
||||
return replace(self, statement=self.statement.where(*criteria))
|
||||
|
||||
def order_by(self, *ordering: Any) -> QuerySet[ModelT]:
|
||||
return replace(self, statement=self.statement.order_by(*ordering))
|
||||
|
||||
def limit(self, value: int) -> QuerySet[ModelT]:
|
||||
return replace(self, statement=self.statement.limit(value))
|
||||
|
||||
def offset(self, value: int) -> QuerySet[ModelT]:
|
||||
return replace(self, statement=self.statement.offset(value))
|
||||
|
||||
async def all(self, session: AsyncSession) -> list[ModelT]:
|
||||
return list(await session.exec(self.statement))
|
||||
|
||||
async def first(self, session: AsyncSession) -> ModelT | None:
|
||||
return (await session.exec(self.statement)).first()
|
||||
|
||||
async def one_or_none(self, session: AsyncSession) -> ModelT | None:
|
||||
return (await session.exec(self.statement)).one_or_none()
|
||||
|
||||
async def exists(self, session: AsyncSession) -> bool:
|
||||
return await self.limit(1).first(session) is not None
|
||||
|
||||
|
||||
def qs(model: type[ModelT]) -> QuerySet[ModelT]:
|
||||
return QuerySet(select(model))
|
||||
@@ -5,12 +5,12 @@ from collections.abc import AsyncGenerator
|
||||
from pathlib import Path
|
||||
|
||||
import anyio
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine
|
||||
from sqlmodel import SQLModel
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from app import models # noqa: F401
|
||||
from app.core.config import settings
|
||||
|
||||
@@ -51,12 +51,12 @@ def run_migrations() -> None:
|
||||
|
||||
async def init_db() -> None:
|
||||
if settings.db_auto_migrate:
|
||||
versions_dir = Path(__file__).resolve().parents[2] / "alembic" / "versions"
|
||||
versions_dir = Path(__file__).resolve().parents[2] / "migrations" / "versions"
|
||||
if any(versions_dir.glob("*.py")):
|
||||
logger.info("Running Alembic migrations on startup")
|
||||
logger.info("Running migrations on startup")
|
||||
await anyio.to_thread.run_sync(run_migrations)
|
||||
return
|
||||
logger.warning("No Alembic revisions found; falling back to create_all")
|
||||
logger.warning("No migration revisions found; falling back to create_all")
|
||||
|
||||
async with async_engine.begin() as conn:
|
||||
await conn.run_sync(SQLModel.metadata.create_all)
|
||||
|
||||
9
backend/app/db/sqlmodel_exec.py
Normal file
9
backend/app/db/sqlmodel_exec.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy.sql.base import Executable
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
|
||||
async def exec_dml(session: AsyncSession, statement: Executable) -> None:
|
||||
# SQLModel's AsyncSession typing only overloads exec() for SELECT statements.
|
||||
await session.exec(statement) # type: ignore[call-overload]
|
||||
1
backend/app/queries/__init__.py
Normal file
1
backend/app/queries/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from __future__ import annotations
|
||||
50
backend/app/queries/organizations.py
Normal file
50
backend/app/queries/organizations.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from sqlmodel import col
|
||||
|
||||
from app.db.queryset import QuerySet, qs
|
||||
from app.models.organization_board_access import OrganizationBoardAccess
|
||||
from app.models.organization_invites import OrganizationInvite
|
||||
from app.models.organization_members import OrganizationMember
|
||||
from app.models.organizations import Organization
|
||||
|
||||
|
||||
def organization_by_name(name: str) -> QuerySet[Organization]:
|
||||
return qs(Organization).filter(col(Organization.name) == name)
|
||||
|
||||
|
||||
def member_by_user_and_org(*, user_id: UUID, organization_id: UUID) -> QuerySet[OrganizationMember]:
|
||||
return qs(OrganizationMember).filter(
|
||||
col(OrganizationMember.organization_id) == organization_id,
|
||||
col(OrganizationMember.user_id) == user_id,
|
||||
)
|
||||
|
||||
|
||||
def first_membership_for_user(user_id: UUID) -> QuerySet[OrganizationMember]:
|
||||
return (
|
||||
qs(OrganizationMember)
|
||||
.filter(col(OrganizationMember.user_id) == user_id)
|
||||
.order_by(col(OrganizationMember.created_at).asc())
|
||||
)
|
||||
|
||||
|
||||
def pending_invite_by_email(email: str) -> QuerySet[OrganizationInvite]:
|
||||
return (
|
||||
qs(OrganizationInvite)
|
||||
.filter(col(OrganizationInvite.accepted_at).is_(None))
|
||||
.filter(col(OrganizationInvite.invited_email) == email)
|
||||
.order_by(col(OrganizationInvite.created_at).asc())
|
||||
)
|
||||
|
||||
|
||||
def board_access_for_member_and_board(
|
||||
*,
|
||||
organization_member_id: UUID,
|
||||
board_id: UUID,
|
||||
) -> QuerySet[OrganizationBoardAccess]:
|
||||
return qs(OrganizationBoardAccess).filter(
|
||||
col(OrganizationBoardAccess.organization_member_id) == organization_member_id,
|
||||
col(OrganizationBoardAccess.board_id) == board_id,
|
||||
)
|
||||
@@ -11,6 +11,7 @@ from sqlmodel import col, select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.core.time import utcnow
|
||||
from app.db.sqlmodel_exec import exec_dml
|
||||
from app.models.boards import Board
|
||||
from app.models.organization_board_access import OrganizationBoardAccess
|
||||
from app.models.organization_invite_board_access import OrganizationInviteBoardAccess
|
||||
@@ -18,6 +19,7 @@ from app.models.organization_invites import OrganizationInvite
|
||||
from app.models.organization_members import OrganizationMember
|
||||
from app.models.organizations import Organization
|
||||
from app.models.users import User
|
||||
from app.queries import organizations as org_queries
|
||||
from app.schemas.organizations import OrganizationBoardAccessSpec, OrganizationMemberAccessUpdate
|
||||
|
||||
DEFAULT_ORG_NAME = "Personal"
|
||||
@@ -36,8 +38,7 @@ def is_org_admin(member: OrganizationMember) -> bool:
|
||||
|
||||
|
||||
async def get_default_org(session: AsyncSession) -> Organization | None:
|
||||
statement = select(Organization).where(col(Organization.name) == DEFAULT_ORG_NAME)
|
||||
return (await session.exec(statement)).first()
|
||||
return await org_queries.organization_by_name(DEFAULT_ORG_NAME).first(session)
|
||||
|
||||
|
||||
async def ensure_default_org(session: AsyncSession) -> Organization:
|
||||
@@ -57,20 +58,14 @@ async def get_member(
|
||||
user_id: UUID,
|
||||
organization_id: UUID,
|
||||
) -> OrganizationMember | None:
|
||||
statement = select(OrganizationMember).where(
|
||||
col(OrganizationMember.organization_id) == organization_id,
|
||||
col(OrganizationMember.user_id) == user_id,
|
||||
)
|
||||
return (await session.exec(statement)).first()
|
||||
return await org_queries.member_by_user_and_org(
|
||||
user_id=user_id,
|
||||
organization_id=organization_id,
|
||||
).first(session)
|
||||
|
||||
|
||||
async def get_first_membership(session: AsyncSession, user_id: UUID) -> OrganizationMember | None:
|
||||
statement = (
|
||||
select(OrganizationMember)
|
||||
.where(col(OrganizationMember.user_id) == user_id)
|
||||
.order_by(col(OrganizationMember.created_at).asc())
|
||||
)
|
||||
return (await session.exec(statement)).first()
|
||||
return await org_queries.first_membership_for_user(user_id).first(session)
|
||||
|
||||
|
||||
async def set_active_organization(
|
||||
@@ -124,13 +119,7 @@ async def _find_pending_invite(
|
||||
session: AsyncSession,
|
||||
email: str,
|
||||
) -> OrganizationInvite | None:
|
||||
statement = (
|
||||
select(OrganizationInvite)
|
||||
.where(col(OrganizationInvite.accepted_at).is_(None))
|
||||
.where(col(OrganizationInvite.invited_email) == email)
|
||||
.order_by(col(OrganizationInvite.created_at).asc())
|
||||
)
|
||||
return (await session.exec(statement)).first()
|
||||
return await org_queries.pending_invite_by_email(email).first(session)
|
||||
|
||||
|
||||
async def accept_invite(
|
||||
@@ -241,11 +230,10 @@ async def has_board_access(
|
||||
else:
|
||||
if member_all_boards_read(member):
|
||||
return True
|
||||
statement = select(OrganizationBoardAccess).where(
|
||||
col(OrganizationBoardAccess.organization_member_id) == member.id,
|
||||
col(OrganizationBoardAccess.board_id) == board.id,
|
||||
)
|
||||
access = (await session.exec(statement)).first()
|
||||
access = await org_queries.board_access_for_member_and_board(
|
||||
organization_member_id=member.id,
|
||||
board_id=board.id,
|
||||
).first(session)
|
||||
if access is None:
|
||||
return False
|
||||
if write:
|
||||
@@ -330,7 +318,8 @@ async def apply_member_access_update(
|
||||
member.updated_at = now
|
||||
session.add(member)
|
||||
|
||||
await session.execute(
|
||||
await exec_dml(
|
||||
session,
|
||||
delete(OrganizationBoardAccess).where(
|
||||
col(OrganizationBoardAccess.organization_member_id) == member.id
|
||||
),
|
||||
@@ -360,7 +349,8 @@ async def apply_invite_board_access(
|
||||
invite: OrganizationInvite,
|
||||
entries: Iterable[OrganizationBoardAccessSpec],
|
||||
) -> None:
|
||||
await session.execute(
|
||||
await exec_dml(
|
||||
session,
|
||||
delete(OrganizationInviteBoardAccess).where(
|
||||
col(OrganizationInviteBoardAccess.organization_invite_id) == invite.id
|
||||
),
|
||||
|
||||
@@ -10,6 +10,7 @@ from sqlalchemy import delete
|
||||
from sqlmodel import col, select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.db.sqlmodel_exec import exec_dml
|
||||
from app.models.task_dependencies import TaskDependency
|
||||
from app.models.tasks import Task
|
||||
|
||||
@@ -194,10 +195,11 @@ async def replace_task_dependencies(
|
||||
task_id=task_id,
|
||||
depends_on_task_ids=depends_on_task_ids,
|
||||
)
|
||||
await session.execute(
|
||||
await exec_dml(
|
||||
session,
|
||||
delete(TaskDependency)
|
||||
.where(col(TaskDependency.board_id) == board_id)
|
||||
.where(col(TaskDependency.task_id) == task_id)
|
||||
.where(col(TaskDependency.task_id) == task_id),
|
||||
)
|
||||
for dep_id in normalized:
|
||||
session.add(
|
||||
|
||||
678
backend/migrations/versions/658dca8f4a11_init.py
Normal file
678
backend/migrations/versions/658dca8f4a11_init.py
Normal file
@@ -0,0 +1,678 @@
|
||||
"""init
|
||||
|
||||
Revision ID: 658dca8f4a11
|
||||
Revises:
|
||||
Create Date: 2026-02-09 00:41:55.760624
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "658dca8f4a11"
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"organizations",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("name", name="uq_organizations_name"),
|
||||
)
|
||||
op.create_index(op.f("ix_organizations_name"), "organizations", ["name"], unique=False)
|
||||
op.create_table(
|
||||
"board_groups",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("organization_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("slug", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["organization_id"],
|
||||
["organizations.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_board_groups_organization_id"), "board_groups", ["organization_id"], unique=False
|
||||
)
|
||||
op.create_index(op.f("ix_board_groups_slug"), "board_groups", ["slug"], unique=False)
|
||||
op.create_table(
|
||||
"gateways",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("organization_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("url", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("token", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("main_session_key", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("workspace_root", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["organization_id"],
|
||||
["organizations.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_gateways_organization_id"), "gateways", ["organization_id"], unique=False
|
||||
)
|
||||
op.create_table(
|
||||
"users",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("clerk_user_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("email", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("preferred_name", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("pronouns", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("timezone", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("notes", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("context", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("is_super_admin", sa.Boolean(), nullable=False),
|
||||
sa.Column("active_organization_id", sa.Uuid(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["active_organization_id"],
|
||||
["organizations.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_users_active_organization_id"), "users", ["active_organization_id"], unique=False
|
||||
)
|
||||
op.create_index(op.f("ix_users_clerk_user_id"), "users", ["clerk_user_id"], unique=True)
|
||||
op.create_index(op.f("ix_users_email"), "users", ["email"], unique=False)
|
||||
op.create_table(
|
||||
"board_group_memory",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("board_group_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("content", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("tags", sa.JSON(), nullable=True),
|
||||
sa.Column("is_chat", sa.Boolean(), nullable=False),
|
||||
sa.Column("source", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["board_group_id"],
|
||||
["board_groups.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_board_group_memory_board_group_id"),
|
||||
"board_group_memory",
|
||||
["board_group_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_board_group_memory_is_chat"), "board_group_memory", ["is_chat"], unique=False
|
||||
)
|
||||
op.create_table(
|
||||
"boards",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("organization_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("slug", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("gateway_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("board_group_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("board_type", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("objective", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("success_metrics", sa.JSON(), nullable=True),
|
||||
sa.Column("target_date", sa.DateTime(), nullable=True),
|
||||
sa.Column("goal_confirmed", sa.Boolean(), nullable=False),
|
||||
sa.Column("goal_source", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["board_group_id"],
|
||||
["board_groups.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["gateway_id"],
|
||||
["gateways.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["organization_id"],
|
||||
["organizations.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_boards_board_group_id"), "boards", ["board_group_id"], unique=False)
|
||||
op.create_index(op.f("ix_boards_board_type"), "boards", ["board_type"], unique=False)
|
||||
op.create_index(op.f("ix_boards_gateway_id"), "boards", ["gateway_id"], unique=False)
|
||||
op.create_index(op.f("ix_boards_organization_id"), "boards", ["organization_id"], unique=False)
|
||||
op.create_index(op.f("ix_boards_slug"), "boards", ["slug"], unique=False)
|
||||
op.create_table(
|
||||
"organization_invites",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("organization_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("invited_email", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("token", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("role", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("all_boards_read", sa.Boolean(), nullable=False),
|
||||
sa.Column("all_boards_write", sa.Boolean(), nullable=False),
|
||||
sa.Column("created_by_user_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("accepted_by_user_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("accepted_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["accepted_by_user_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["created_by_user_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["organization_id"],
|
||||
["organizations.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("token", name="uq_org_invites_token"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_organization_invites_accepted_by_user_id"),
|
||||
"organization_invites",
|
||||
["accepted_by_user_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_organization_invites_created_by_user_id"),
|
||||
"organization_invites",
|
||||
["created_by_user_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_organization_invites_invited_email"),
|
||||
"organization_invites",
|
||||
["invited_email"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_organization_invites_organization_id"),
|
||||
"organization_invites",
|
||||
["organization_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_organization_invites_role"), "organization_invites", ["role"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_organization_invites_token"), "organization_invites", ["token"], unique=False
|
||||
)
|
||||
op.create_table(
|
||||
"organization_members",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("organization_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("user_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("role", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("all_boards_read", sa.Boolean(), nullable=False),
|
||||
sa.Column("all_boards_write", sa.Boolean(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["organization_id"],
|
||||
["organizations.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("organization_id", "user_id", name="uq_organization_members_org_user"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_organization_members_organization_id"),
|
||||
"organization_members",
|
||||
["organization_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_organization_members_role"), "organization_members", ["role"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_organization_members_user_id"), "organization_members", ["user_id"], unique=False
|
||||
)
|
||||
op.create_table(
|
||||
"agents",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("board_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("status", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("openclaw_session_id", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("agent_token_hash", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("heartbeat_config", sa.JSON(), nullable=True),
|
||||
sa.Column("identity_profile", sa.JSON(), nullable=True),
|
||||
sa.Column("identity_template", sa.Text(), nullable=True),
|
||||
sa.Column("soul_template", sa.Text(), nullable=True),
|
||||
sa.Column("provision_requested_at", sa.DateTime(), nullable=True),
|
||||
sa.Column(
|
||||
"provision_confirm_token_hash", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
||||
),
|
||||
sa.Column("provision_action", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("delete_requested_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("delete_confirm_token_hash", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("last_seen_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("is_board_lead", sa.Boolean(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["board_id"],
|
||||
["boards.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_agents_agent_token_hash"), "agents", ["agent_token_hash"], unique=False
|
||||
)
|
||||
op.create_index(op.f("ix_agents_board_id"), "agents", ["board_id"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_agents_delete_confirm_token_hash"),
|
||||
"agents",
|
||||
["delete_confirm_token_hash"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(op.f("ix_agents_is_board_lead"), "agents", ["is_board_lead"], unique=False)
|
||||
op.create_index(op.f("ix_agents_name"), "agents", ["name"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_agents_openclaw_session_id"), "agents", ["openclaw_session_id"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_agents_provision_action"), "agents", ["provision_action"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_agents_provision_confirm_token_hash"),
|
||||
"agents",
|
||||
["provision_confirm_token_hash"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(op.f("ix_agents_status"), "agents", ["status"], unique=False)
|
||||
op.create_table(
|
||||
"board_memory",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("board_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("content", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("tags", sa.JSON(), nullable=True),
|
||||
sa.Column("is_chat", sa.Boolean(), nullable=False),
|
||||
sa.Column("source", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["board_id"],
|
||||
["boards.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_board_memory_board_id"), "board_memory", ["board_id"], unique=False)
|
||||
op.create_index(op.f("ix_board_memory_is_chat"), "board_memory", ["is_chat"], unique=False)
|
||||
op.create_table(
|
||||
"board_onboarding_sessions",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("board_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("session_key", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("status", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("messages", sa.JSON(), nullable=True),
|
||||
sa.Column("draft_goal", sa.JSON(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["board_id"],
|
||||
["boards.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_board_onboarding_sessions_board_id"),
|
||||
"board_onboarding_sessions",
|
||||
["board_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_board_onboarding_sessions_status"),
|
||||
"board_onboarding_sessions",
|
||||
["status"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_table(
|
||||
"organization_board_access",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("organization_member_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("board_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("can_read", sa.Boolean(), nullable=False),
|
||||
sa.Column("can_write", sa.Boolean(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["board_id"],
|
||||
["boards.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["organization_member_id"],
|
||||
["organization_members.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint(
|
||||
"organization_member_id", "board_id", name="uq_org_board_access_member_board"
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_organization_board_access_board_id"),
|
||||
"organization_board_access",
|
||||
["board_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_organization_board_access_organization_member_id"),
|
||||
"organization_board_access",
|
||||
["organization_member_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_table(
|
||||
"organization_invite_board_access",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("organization_invite_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("board_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("can_read", sa.Boolean(), nullable=False),
|
||||
sa.Column("can_write", sa.Boolean(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["board_id"],
|
||||
["boards.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["organization_invite_id"],
|
||||
["organization_invites.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint(
|
||||
"organization_invite_id", "board_id", name="uq_org_invite_board_access_invite_board"
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_organization_invite_board_access_board_id"),
|
||||
"organization_invite_board_access",
|
||||
["board_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_organization_invite_board_access_organization_invite_id"),
|
||||
"organization_invite_board_access",
|
||||
["organization_invite_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_table(
|
||||
"tasks",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("board_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("title", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("status", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("priority", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("due_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("in_progress_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("created_by_user_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("assigned_agent_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("auto_created", sa.Boolean(), nullable=False),
|
||||
sa.Column("auto_reason", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["assigned_agent_id"],
|
||||
["agents.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["board_id"],
|
||||
["boards.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["created_by_user_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_tasks_assigned_agent_id"), "tasks", ["assigned_agent_id"], unique=False
|
||||
)
|
||||
op.create_index(op.f("ix_tasks_board_id"), "tasks", ["board_id"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_tasks_created_by_user_id"), "tasks", ["created_by_user_id"], unique=False
|
||||
)
|
||||
op.create_index(op.f("ix_tasks_priority"), "tasks", ["priority"], unique=False)
|
||||
op.create_index(op.f("ix_tasks_status"), "tasks", ["status"], unique=False)
|
||||
op.create_table(
|
||||
"activity_events",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("event_type", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("message", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("agent_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("task_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["agent_id"],
|
||||
["agents.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["task_id"],
|
||||
["tasks.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_activity_events_agent_id"), "activity_events", ["agent_id"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_activity_events_event_type"), "activity_events", ["event_type"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_activity_events_task_id"), "activity_events", ["task_id"], unique=False
|
||||
)
|
||||
op.create_table(
|
||||
"approvals",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("board_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("task_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("agent_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("action_type", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("payload", sa.JSON(), nullable=True),
|
||||
sa.Column("confidence", sa.Integer(), nullable=False),
|
||||
sa.Column("rubric_scores", sa.JSON(), nullable=True),
|
||||
sa.Column("status", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("resolved_at", sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["agent_id"],
|
||||
["agents.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["board_id"],
|
||||
["boards.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["task_id"],
|
||||
["tasks.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_approvals_agent_id"), "approvals", ["agent_id"], unique=False)
|
||||
op.create_index(op.f("ix_approvals_board_id"), "approvals", ["board_id"], unique=False)
|
||||
op.create_index(op.f("ix_approvals_status"), "approvals", ["status"], unique=False)
|
||||
op.create_index(op.f("ix_approvals_task_id"), "approvals", ["task_id"], unique=False)
|
||||
op.create_table(
|
||||
"task_dependencies",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("board_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("task_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("depends_on_task_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.CheckConstraint("task_id <> depends_on_task_id", name="ck_task_dependencies_no_self"),
|
||||
sa.ForeignKeyConstraint(
|
||||
["board_id"],
|
||||
["boards.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["depends_on_task_id"],
|
||||
["tasks.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["task_id"],
|
||||
["tasks.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint(
|
||||
"task_id", "depends_on_task_id", name="uq_task_dependencies_task_id_depends_on_task_id"
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_task_dependencies_board_id"), "task_dependencies", ["board_id"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_task_dependencies_depends_on_task_id"),
|
||||
"task_dependencies",
|
||||
["depends_on_task_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_task_dependencies_task_id"), "task_dependencies", ["task_id"], unique=False
|
||||
)
|
||||
op.create_table(
|
||||
"task_fingerprints",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("board_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("fingerprint_hash", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("task_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["board_id"],
|
||||
["boards.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["task_id"],
|
||||
["tasks.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_task_fingerprints_board_id"), "task_fingerprints", ["board_id"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_task_fingerprints_fingerprint_hash"),
|
||||
"task_fingerprints",
|
||||
["fingerprint_hash"],
|
||||
unique=False,
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f("ix_task_fingerprints_fingerprint_hash"), table_name="task_fingerprints")
|
||||
op.drop_index(op.f("ix_task_fingerprints_board_id"), table_name="task_fingerprints")
|
||||
op.drop_table("task_fingerprints")
|
||||
op.drop_index(op.f("ix_task_dependencies_task_id"), table_name="task_dependencies")
|
||||
op.drop_index(op.f("ix_task_dependencies_depends_on_task_id"), table_name="task_dependencies")
|
||||
op.drop_index(op.f("ix_task_dependencies_board_id"), table_name="task_dependencies")
|
||||
op.drop_table("task_dependencies")
|
||||
op.drop_index(op.f("ix_approvals_task_id"), table_name="approvals")
|
||||
op.drop_index(op.f("ix_approvals_status"), table_name="approvals")
|
||||
op.drop_index(op.f("ix_approvals_board_id"), table_name="approvals")
|
||||
op.drop_index(op.f("ix_approvals_agent_id"), table_name="approvals")
|
||||
op.drop_table("approvals")
|
||||
op.drop_index(op.f("ix_activity_events_task_id"), table_name="activity_events")
|
||||
op.drop_index(op.f("ix_activity_events_event_type"), table_name="activity_events")
|
||||
op.drop_index(op.f("ix_activity_events_agent_id"), table_name="activity_events")
|
||||
op.drop_table("activity_events")
|
||||
op.drop_index(op.f("ix_tasks_status"), table_name="tasks")
|
||||
op.drop_index(op.f("ix_tasks_priority"), table_name="tasks")
|
||||
op.drop_index(op.f("ix_tasks_created_by_user_id"), table_name="tasks")
|
||||
op.drop_index(op.f("ix_tasks_board_id"), table_name="tasks")
|
||||
op.drop_index(op.f("ix_tasks_assigned_agent_id"), table_name="tasks")
|
||||
op.drop_table("tasks")
|
||||
op.drop_index(
|
||||
op.f("ix_organization_invite_board_access_organization_invite_id"),
|
||||
table_name="organization_invite_board_access",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_organization_invite_board_access_board_id"),
|
||||
table_name="organization_invite_board_access",
|
||||
)
|
||||
op.drop_table("organization_invite_board_access")
|
||||
op.drop_index(
|
||||
op.f("ix_organization_board_access_organization_member_id"),
|
||||
table_name="organization_board_access",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_organization_board_access_board_id"), table_name="organization_board_access"
|
||||
)
|
||||
op.drop_table("organization_board_access")
|
||||
op.drop_index(
|
||||
op.f("ix_board_onboarding_sessions_status"), table_name="board_onboarding_sessions"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_board_onboarding_sessions_board_id"), table_name="board_onboarding_sessions"
|
||||
)
|
||||
op.drop_table("board_onboarding_sessions")
|
||||
op.drop_index(op.f("ix_board_memory_is_chat"), table_name="board_memory")
|
||||
op.drop_index(op.f("ix_board_memory_board_id"), table_name="board_memory")
|
||||
op.drop_table("board_memory")
|
||||
op.drop_index(op.f("ix_agents_status"), table_name="agents")
|
||||
op.drop_index(op.f("ix_agents_provision_confirm_token_hash"), table_name="agents")
|
||||
op.drop_index(op.f("ix_agents_provision_action"), table_name="agents")
|
||||
op.drop_index(op.f("ix_agents_openclaw_session_id"), table_name="agents")
|
||||
op.drop_index(op.f("ix_agents_name"), table_name="agents")
|
||||
op.drop_index(op.f("ix_agents_is_board_lead"), table_name="agents")
|
||||
op.drop_index(op.f("ix_agents_delete_confirm_token_hash"), table_name="agents")
|
||||
op.drop_index(op.f("ix_agents_board_id"), table_name="agents")
|
||||
op.drop_index(op.f("ix_agents_agent_token_hash"), table_name="agents")
|
||||
op.drop_table("agents")
|
||||
op.drop_index(op.f("ix_organization_members_user_id"), table_name="organization_members")
|
||||
op.drop_index(op.f("ix_organization_members_role"), table_name="organization_members")
|
||||
op.drop_index(
|
||||
op.f("ix_organization_members_organization_id"), table_name="organization_members"
|
||||
)
|
||||
op.drop_table("organization_members")
|
||||
op.drop_index(op.f("ix_organization_invites_token"), table_name="organization_invites")
|
||||
op.drop_index(op.f("ix_organization_invites_role"), table_name="organization_invites")
|
||||
op.drop_index(
|
||||
op.f("ix_organization_invites_organization_id"), table_name="organization_invites"
|
||||
)
|
||||
op.drop_index(op.f("ix_organization_invites_invited_email"), table_name="organization_invites")
|
||||
op.drop_index(
|
||||
op.f("ix_organization_invites_created_by_user_id"), table_name="organization_invites"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_organization_invites_accepted_by_user_id"), table_name="organization_invites"
|
||||
)
|
||||
op.drop_table("organization_invites")
|
||||
op.drop_index(op.f("ix_boards_slug"), table_name="boards")
|
||||
op.drop_index(op.f("ix_boards_organization_id"), table_name="boards")
|
||||
op.drop_index(op.f("ix_boards_gateway_id"), table_name="boards")
|
||||
op.drop_index(op.f("ix_boards_board_type"), table_name="boards")
|
||||
op.drop_index(op.f("ix_boards_board_group_id"), table_name="boards")
|
||||
op.drop_table("boards")
|
||||
op.drop_index(op.f("ix_board_group_memory_is_chat"), table_name="board_group_memory")
|
||||
op.drop_index(op.f("ix_board_group_memory_board_group_id"), table_name="board_group_memory")
|
||||
op.drop_table("board_group_memory")
|
||||
op.drop_index(op.f("ix_users_email"), table_name="users")
|
||||
op.drop_index(op.f("ix_users_clerk_user_id"), table_name="users")
|
||||
op.drop_index(op.f("ix_users_active_organization_id"), table_name="users")
|
||||
op.drop_table("users")
|
||||
op.drop_index(op.f("ix_gateways_organization_id"), table_name="gateways")
|
||||
op.drop_table("gateways")
|
||||
op.drop_index(op.f("ix_board_groups_slug"), table_name="board_groups")
|
||||
op.drop_index(op.f("ix_board_groups_organization_id"), table_name="board_groups")
|
||||
op.drop_table("board_groups")
|
||||
op.drop_index(op.f("ix_organizations_name"), table_name="organizations")
|
||||
op.drop_table("organizations")
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,12 +1,12 @@
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
target-version = ["py312"]
|
||||
extend-exclude = '(\.venv|alembic/versions)'
|
||||
extend-exclude = '(\.venv|migrations/versions)'
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
line_length = 100
|
||||
skip = [".venv", "alembic/versions"]
|
||||
skip = [".venv", "migrations/versions"]
|
||||
[project]
|
||||
name = "openclaw-agency-backend"
|
||||
version = "0.1.0"
|
||||
@@ -59,7 +59,7 @@ asyncio_default_fixture_loop_scope = "function"
|
||||
branch = true
|
||||
source = ["app"]
|
||||
# Migrations are generated artifacts; testing them doesn't add coverage signal.
|
||||
omit = ["alembic/versions/*"]
|
||||
omit = ["migrations/versions/*"]
|
||||
|
||||
[tool.coverage.report]
|
||||
show_missing = true
|
||||
|
||||
@@ -15,6 +15,9 @@ class _FakeSession:
|
||||
executed: list[Any] = field(default_factory=list)
|
||||
committed: int = 0
|
||||
|
||||
async def exec(self, statement: Any) -> None:
|
||||
self.executed.append(statement)
|
||||
|
||||
async def execute(self, statement: Any) -> None:
|
||||
self.executed.append(statement)
|
||||
|
||||
|
||||
@@ -17,7 +17,11 @@ class _FakeSession:
|
||||
deleted: list[Any] = field(default_factory=list)
|
||||
committed: int = 0
|
||||
|
||||
async def exec(self, _statement: Any) -> Any:
|
||||
async def exec(self, statement: Any) -> Any:
|
||||
is_dml = statement.__class__.__name__ in {"Delete", "Update", "Insert"}
|
||||
if is_dml:
|
||||
self.executed.append(statement)
|
||||
return None
|
||||
if not self.exec_results:
|
||||
raise AssertionError("No more exec_results left for session.exec")
|
||||
return self.exec_results.pop(0)
|
||||
|
||||
@@ -16,6 +16,9 @@ class _FakeSession:
|
||||
executed: list[Any] = field(default_factory=list)
|
||||
committed: int = 0
|
||||
|
||||
async def exec(self, statement: Any) -> None:
|
||||
self.executed.append(statement)
|
||||
|
||||
async def execute(self, statement: Any) -> None:
|
||||
self.executed.append(statement)
|
||||
|
||||
|
||||
@@ -36,6 +36,10 @@ class _FakeSession:
|
||||
committed: int = 0
|
||||
|
||||
async def exec(self, _statement: Any) -> Any:
|
||||
is_dml = _statement.__class__.__name__ in {"Delete", "Update", "Insert"}
|
||||
if is_dml:
|
||||
self.executed.append(_statement)
|
||||
return None
|
||||
if not self.exec_results:
|
||||
raise AssertionError("No more exec_results left for session.exec")
|
||||
return self.exec_results.pop(0)
|
||||
|
||||
@@ -51,6 +51,10 @@ class _FakeSession:
|
||||
refreshed: list[Any] = field(default_factory=list)
|
||||
|
||||
async def exec(self, _statement: Any) -> Any:
|
||||
is_dml = _statement.__class__.__name__ in {"Delete", "Update", "Insert"}
|
||||
if is_dml:
|
||||
self.executed.append(_statement)
|
||||
return None
|
||||
if not self.exec_results:
|
||||
raise AssertionError("No more exec_results left for session.exec")
|
||||
return self.exec_results.pop(0)
|
||||
|
||||
@@ -68,6 +68,10 @@ class _FakeSession:
|
||||
added: list[object] = field(default_factory=list)
|
||||
|
||||
async def exec(self, _query):
|
||||
is_dml = _query.__class__.__name__ in {"Delete", "Update", "Insert"}
|
||||
if is_dml:
|
||||
self.executed.append(_query)
|
||||
return None
|
||||
if not self.exec_results:
|
||||
raise AssertionError("No more exec_results left for session.exec")
|
||||
return self.exec_results.pop(0)
|
||||
|
||||
@@ -49,7 +49,7 @@ flowchart LR
|
||||
### Data stores
|
||||
- **Postgres**: persistence for boards/tasks/agents/approvals/etc.
|
||||
- Models: `backend/app/models/*`
|
||||
- Migrations: `backend/alembic/*`
|
||||
- Migrations: `backend/migrations/*`
|
||||
- **Redis**: used for background primitives.
|
||||
- RQ helper: `backend/app/workers/queue.py`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user