fix(gate-b): fresh-db install path - conditional guards on plugin-table migrations + dual-path convergence migrations
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
This commit is contained in:
@@ -18,7 +18,26 @@ branch_labels = None
|
|||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def _table_exists(conn, table_name: str) -> bool:
|
||||||
|
"""True when the table exists (dual-path convergence, Gate B).
|
||||||
|
|
||||||
|
On a fresh install the ai_assistant plugin SQL migration has not run
|
||||||
|
yet when Alembic reaches this revision — skip instead of failing.
|
||||||
|
The plugin-side migration adds the same columns idempotently.
|
||||||
|
"""
|
||||||
|
row = conn.execute(
|
||||||
|
sa.text("SELECT to_regclass(:tname) IS NOT NULL"),
|
||||||
|
{"tname": f"public.{table_name}"},
|
||||||
|
).scalar()
|
||||||
|
return bool(row)
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
if not _table_exists(conn, "ai_providers"):
|
||||||
|
# Fresh-install path: table arrives with the ai_assistant plugin
|
||||||
|
# migration, which includes these columns.
|
||||||
|
return
|
||||||
op.add_column("ai_providers", sa.Column("region", sa.String(20), nullable=False, server_default="unknown"))
|
op.add_column("ai_providers", sa.Column("region", sa.String(20), nullable=False, server_default="unknown"))
|
||||||
op.add_column("ai_providers", sa.Column("hosting_type", sa.String(30), nullable=False, server_default="cloud"))
|
op.add_column("ai_providers", sa.Column("hosting_type", sa.String(30), nullable=False, server_default="cloud"))
|
||||||
op.add_column("ai_providers", sa.Column("dpa_status", sa.String(20), nullable=False, server_default="none"))
|
op.add_column("ai_providers", sa.Column("dpa_status", sa.String(20), nullable=False, server_default="none"))
|
||||||
@@ -29,6 +48,9 @@ def upgrade() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
if not _table_exists(conn, "ai_providers"):
|
||||||
|
return
|
||||||
op.drop_column("ai_providers", "allowed_data_classes")
|
op.drop_column("ai_providers", "allowed_data_classes")
|
||||||
op.drop_column("ai_providers", "transfer_notice")
|
op.drop_column("ai_providers", "transfer_notice")
|
||||||
op.drop_column("ai_providers", "training_on_customer_data")
|
op.drop_column("ai_providers", "training_on_customer_data")
|
||||||
|
|||||||
@@ -17,126 +17,145 @@ branch_labels = None
|
|||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def _table_exists(conn, table_name: str) -> bool:
|
||||||
|
"""True when the table exists (dual-path convergence, Gate B).
|
||||||
|
|
||||||
|
On a fresh install the kommunikation plugin SQL migration has not run
|
||||||
|
yet when Alembic reaches this revision — skip the comm_* parts instead
|
||||||
|
of failing. The plugin-side migration adds the same column idempotently.
|
||||||
|
"""
|
||||||
|
row = conn.execute(
|
||||||
|
sa.text("SELECT to_regclass(:tname) IS NOT NULL"),
|
||||||
|
{"tname": f"public.{table_name}"},
|
||||||
|
).scalar()
|
||||||
|
return bool(row)
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# 1. Add is_system column to comm_conversations
|
conn = op.get_bind()
|
||||||
op.add_column(
|
if _table_exists(conn, "comm_conversations"):
|
||||||
"comm_conversations",
|
# 1. Add is_system column to comm_conversations
|
||||||
sa.Column("is_system", sa.Boolean(), nullable=False, server_default=sa.text("false")),
|
op.add_column(
|
||||||
)
|
"comm_conversations",
|
||||||
op.create_index(
|
sa.Column("is_system", sa.Boolean(), nullable=False, server_default=sa.text("false")),
|
||||||
"ix_comm_conversations_tenant_system",
|
)
|
||||||
"comm_conversations",
|
op.create_index(
|
||||||
["tenant_id", "is_system"],
|
"ix_comm_conversations_tenant_system",
|
||||||
)
|
"comm_conversations",
|
||||||
|
["tenant_id", "is_system"],
|
||||||
|
)
|
||||||
|
|
||||||
# 2. Create system channel per tenant (for tenants that have notifications)
|
# 2. Create system channel per tenant (for tenants that have notifications)
|
||||||
op.execute("""
|
op.execute("""
|
||||||
INSERT INTO comm_conversations (id, tenant_id, title, is_pinned, is_locked, is_direct, is_archived, is_system, created_by, created_by_type, metadata, created_at, updated_at)
|
INSERT INTO comm_conversations (id, tenant_id, title, is_pinned, is_locked, is_direct, is_archived, is_system, created_by, created_by_type, metadata, created_at, updated_at)
|
||||||
SELECT
|
SELECT
|
||||||
gen_random_uuid(),
|
gen_random_uuid(),
|
||||||
n.tenant_id,
|
n.tenant_id,
|
||||||
'System Channel',
|
'System Channel',
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
NULL,
|
NULL,
|
||||||
'system',
|
'system',
|
||||||
'{}'::jsonb,
|
'{}'::jsonb,
|
||||||
NOW(),
|
NOW(),
|
||||||
NOW()
|
NOW()
|
||||||
FROM (
|
FROM (
|
||||||
SELECT DISTINCT tenant_id FROM notifications WHERE deleted_at IS NULL
|
SELECT DISTINCT tenant_id FROM notifications WHERE deleted_at IS NULL
|
||||||
) n
|
) n
|
||||||
WHERE NOT EXISTS (
|
WHERE NOT EXISTS (
|
||||||
SELECT 1 FROM comm_conversations cc
|
SELECT 1 FROM comm_conversations cc
|
||||||
WHERE cc.tenant_id = n.tenant_id AND cc.is_system = true AND cc.deleted_at IS NULL
|
WHERE cc.tenant_id = n.tenant_id AND cc.is_system = true AND cc.deleted_at IS NULL
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# 3. Insert notifications as CommMessages in the system channel
|
# 3. Insert notifications as CommMessages in the system channel
|
||||||
op.execute("""
|
op.execute("""
|
||||||
INSERT INTO comm_messages (id, tenant_id, conversation_id, sender_id, sender_type, content, content_format, metadata, created_at, updated_at)
|
INSERT INTO comm_messages (id, tenant_id, conversation_id, sender_id, sender_type, content, content_format, metadata, created_at, updated_at)
|
||||||
SELECT
|
SELECT
|
||||||
gen_random_uuid(),
|
gen_random_uuid(),
|
||||||
n.tenant_id,
|
n.tenant_id,
|
||||||
sc.id,
|
sc.id,
|
||||||
n.user_id,
|
n.user_id,
|
||||||
'system',
|
'system',
|
||||||
COALESCE(n.title, '') || CASE WHEN n.body IS NOT NULL THEN E'\n' || n.body ELSE '' END,
|
COALESCE(n.title, '') || CASE WHEN n.body IS NOT NULL THEN E'\n' || n.body ELSE '' END,
|
||||||
'text',
|
'text',
|
||||||
jsonb_build_object(
|
jsonb_build_object(
|
||||||
'notification_type', n.type,
|
'notification_type', n.type,
|
||||||
'severity', 'info',
|
'severity', 'info',
|
||||||
'entity_ref', CASE WHEN n.entity_type IS NOT NULL THEN jsonb_build_object('entity_type', n.entity_type, 'entity_id', n.entity_id::text) ELSE NULL END,
|
'entity_ref', CASE WHEN n.entity_type IS NOT NULL THEN jsonb_build_object('entity_type', n.entity_type, 'entity_id', n.entity_id::text) ELSE NULL END,
|
||||||
'migrated_from_notification', true,
|
'migrated_from_notification', true,
|
||||||
'original_notification_id', n.id::text
|
'original_notification_id', n.id::text
|
||||||
),
|
),
|
||||||
n.created_at,
|
n.created_at,
|
||||||
COALESCE(n.read_at, n.created_at)
|
COALESCE(n.read_at, n.created_at)
|
||||||
FROM notifications n
|
FROM notifications n
|
||||||
JOIN comm_conversations sc ON sc.tenant_id = n.tenant_id AND sc.is_system = true AND sc.deleted_at IS NULL
|
JOIN comm_conversations sc ON sc.tenant_id = n.tenant_id AND sc.is_system = true AND sc.deleted_at IS NULL
|
||||||
WHERE n.deleted_at IS NULL;
|
WHERE n.deleted_at IS NULL;
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# 4. Insert text blocks for each migrated message
|
# 4. Insert text blocks for each migrated message
|
||||||
op.execute("""
|
op.execute("""
|
||||||
INSERT INTO comm_message_blocks (id, tenant_id, message_id, block_type, block_data, sort_order)
|
INSERT INTO comm_message_blocks (id, tenant_id, message_id, block_type, block_data, sort_order)
|
||||||
SELECT
|
SELECT
|
||||||
gen_random_uuid(),
|
gen_random_uuid(),
|
||||||
cm.tenant_id,
|
cm.tenant_id,
|
||||||
cm.id,
|
cm.id,
|
||||||
'text',
|
'text',
|
||||||
jsonb_build_object('text', cm.content),
|
jsonb_build_object('text', cm.content),
|
||||||
0
|
0
|
||||||
FROM comm_messages cm
|
FROM comm_messages cm
|
||||||
WHERE cm.metadata->>'migrated_from_notification' = 'true';
|
WHERE cm.metadata->>'migrated_from_notification' = 'true';
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# 5. Insert action_card blocks for messages with entity references
|
# 5. Insert action_card blocks for messages with entity references
|
||||||
op.execute("""
|
op.execute("""
|
||||||
INSERT INTO comm_message_blocks (id, tenant_id, message_id, block_type, block_data, sort_order)
|
INSERT INTO comm_message_blocks (id, tenant_id, message_id, block_type, block_data, sort_order)
|
||||||
SELECT
|
SELECT
|
||||||
gen_random_uuid(),
|
gen_random_uuid(),
|
||||||
cm.tenant_id,
|
cm.tenant_id,
|
||||||
cm.id,
|
cm.id,
|
||||||
'action_card',
|
'action_card',
|
||||||
jsonb_build_object(
|
jsonb_build_object(
|
||||||
'label', 'Open',
|
'label', 'Open',
|
||||||
'entity_type', (cm.metadata->'entity_ref'->>'entity_type'),
|
'entity_type', (cm.metadata->'entity_ref'->>'entity_type'),
|
||||||
'entity_id', (cm.metadata->'entity_ref'->>'entity_id')
|
'entity_id', (cm.metadata->'entity_ref'->>'entity_id')
|
||||||
),
|
),
|
||||||
1
|
1
|
||||||
FROM comm_messages cm
|
FROM comm_messages cm
|
||||||
WHERE cm.metadata->>'migrated_from_notification' = 'true'
|
WHERE cm.metadata->>'migrated_from_notification' = 'true'
|
||||||
AND cm.metadata->'entity_ref' IS NOT NULL;
|
AND cm.metadata->'entity_ref' IS NOT NULL;
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# 6. For read notifications, create CommMessageRead entries
|
# 6. For read notifications, create CommMessageRead entries
|
||||||
op.execute("""
|
op.execute("""
|
||||||
INSERT INTO comm_message_reads (id, tenant_id, conversation_id, user_id, last_read_msg_id, last_read_at)
|
INSERT INTO comm_message_reads (id, tenant_id, conversation_id, user_id, last_read_msg_id, last_read_at)
|
||||||
SELECT
|
SELECT
|
||||||
gen_random_uuid(),
|
gen_random_uuid(),
|
||||||
cm.tenant_id,
|
cm.tenant_id,
|
||||||
cm.conversation_id,
|
cm.conversation_id,
|
||||||
cm.sender_id,
|
cm.sender_id,
|
||||||
cm.id,
|
cm.id,
|
||||||
COALESCE(n.read_at, n.created_at)
|
COALESCE(n.read_at, n.created_at)
|
||||||
FROM comm_messages cm
|
FROM comm_messages cm
|
||||||
JOIN notifications n ON n.id::text = cm.metadata->>'original_notification_id'
|
JOIN notifications n ON n.id::text = cm.metadata->>'original_notification_id'
|
||||||
WHERE cm.metadata->>'migrated_from_notification' = 'true'
|
WHERE cm.metadata->>'migrated_from_notification' = 'true'
|
||||||
AND n.read_at IS NOT NULL
|
AND n.read_at IS NOT NULL
|
||||||
AND n.deleted_at IS NULL;
|
AND n.deleted_at IS NULL;
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# 7. Create legacy view over notifications table for backward compatibility
|
# 7. Legacy view over the CORE notifications table — exists on both paths
|
||||||
op.execute("DROP VIEW IF EXISTS notifications_legacy")
|
op.execute("DROP VIEW IF EXISTS notifications_legacy")
|
||||||
op.execute("CREATE VIEW notifications_legacy AS SELECT * FROM notifications")
|
op.execute("CREATE VIEW notifications_legacy AS SELECT * FROM notifications")
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
op.execute("DROP VIEW IF EXISTS notifications_legacy")
|
op.execute("DROP VIEW IF EXISTS notifications_legacy")
|
||||||
|
if not _table_exists(conn, "comm_conversations"):
|
||||||
|
return
|
||||||
op.execute("DELETE FROM comm_message_blocks WHERE message_id IN (SELECT id FROM comm_messages WHERE metadata->>'migrated_from_notification' = 'true')")
|
op.execute("DELETE FROM comm_message_blocks WHERE message_id IN (SELECT id FROM comm_messages WHERE metadata->>'migrated_from_notification' = 'true')")
|
||||||
op.execute("DELETE FROM comm_messages WHERE metadata->>'migrated_from_notification' = 'true'")
|
op.execute("DELETE FROM comm_messages WHERE metadata->>'migrated_from_notification' = 'true'")
|
||||||
op.execute("DELETE FROM comm_conversations WHERE is_system = true AND title = 'System Channel'")
|
op.execute("DELETE FROM comm_conversations WHERE is_system = true AND title = 'System Channel'")
|
||||||
|
|||||||
@@ -14,7 +14,24 @@ branch_labels = None
|
|||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def _table_exists(conn, table_name: str) -> bool:
|
||||||
|
"""True when the table exists (dual-path convergence, Gate B).
|
||||||
|
|
||||||
|
On a fresh install the automation plugin SQL migration has not run yet
|
||||||
|
when Alembic reaches this revision — skip instead of failing. The
|
||||||
|
plugin-side convergence migration creates the same table.
|
||||||
|
"""
|
||||||
|
row = conn.execute(
|
||||||
|
sa.text("SELECT to_regclass(:tname) IS NOT NULL"),
|
||||||
|
{"tname": f"public.{table_name}"},
|
||||||
|
).scalar()
|
||||||
|
return bool(row)
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
if not _table_exists(conn, "automation_agent_runs"):
|
||||||
|
return
|
||||||
op.create_table(
|
op.create_table(
|
||||||
"automation_agent_run_steps",
|
"automation_agent_run_steps",
|
||||||
sa.Column("id", PGUUID(as_uuid=True), primary_key=True),
|
sa.Column("id", PGUUID(as_uuid=True), primary_key=True),
|
||||||
|
|||||||
@@ -18,7 +18,24 @@ branch_labels = None
|
|||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def _table_exists(conn, table_name: str) -> bool:
|
||||||
|
"""True when the table exists (dual-path convergence, Gate B).
|
||||||
|
|
||||||
|
On a fresh install the automation plugin SQL migration has not run yet
|
||||||
|
when Alembic reaches this revision — skip instead of failing. The
|
||||||
|
plugin-side convergence migration adds the same columns.
|
||||||
|
"""
|
||||||
|
row = conn.execute(
|
||||||
|
sa.text("SELECT to_regclass(:tname) IS NOT NULL"),
|
||||||
|
{"tname": f"public.{table_name}"},
|
||||||
|
).scalar()
|
||||||
|
return bool(row)
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
if not _table_exists(conn, "automation_agent_definitions"):
|
||||||
|
return
|
||||||
op.add_column(
|
op.add_column(
|
||||||
"automation_agent_definitions",
|
"automation_agent_definitions",
|
||||||
sa.Column("temperature", sa.Float, nullable=False, server_default="0.3"),
|
sa.Column("temperature", sa.Float, nullable=False, server_default="0.3"),
|
||||||
|
|||||||
@@ -20,7 +20,27 @@ branch_labels = None
|
|||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def _table_exists(conn, table_name: str) -> bool:
|
||||||
|
"""True when the table exists (dual-path convergence, Gate B).
|
||||||
|
|
||||||
|
On a fresh install the tasks plugin SQL migration has not run yet when
|
||||||
|
Alembic reaches this revision — skip instead of failing. The plugin-side
|
||||||
|
convergence migration adds the same columns/indexes. The legacy-data
|
||||||
|
backfills below only matter for pre-existing rows and are correctly
|
||||||
|
empty on a fresh install.
|
||||||
|
"""
|
||||||
|
row = conn.execute(
|
||||||
|
sa.text("SELECT to_regclass(:tname) IS NOT NULL"),
|
||||||
|
{"tname": f"public.{table_name}"},
|
||||||
|
).scalar()
|
||||||
|
return bool(row)
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
if not _table_exists(conn, "tasks"):
|
||||||
|
return
|
||||||
|
|
||||||
# ── Add new columns to tasks ────────────────────────────────────────────
|
# ── Add new columns to tasks ────────────────────────────────────────────
|
||||||
op.add_column("tasks", sa.Column("assignee_type", sa.String(20), nullable=False, server_default="user"))
|
op.add_column("tasks", sa.Column("assignee_type", sa.String(20), nullable=False, server_default="user"))
|
||||||
op.add_column("tasks", sa.Column("assignee_id", PGUUID(as_uuid=True), nullable=True))
|
op.add_column("tasks", sa.Column("assignee_id", PGUUID(as_uuid=True), nullable=True))
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ Revises: 0126
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
revision = "0127"
|
revision = "0127"
|
||||||
down_revision = "0126"
|
down_revision = "0126"
|
||||||
@@ -19,7 +20,19 @@ branch_labels = None
|
|||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def _table_exists(conn, table_name: str) -> bool:
|
||||||
|
"""True when the table exists (dual-path convergence, Gate B)."""
|
||||||
|
row = conn.execute(
|
||||||
|
sa.text("SELECT to_regclass(:tname) IS NOT NULL"),
|
||||||
|
{"tname": f"public.{table_name}"},
|
||||||
|
).scalar()
|
||||||
|
return bool(row)
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
if not _table_exists(conn, "tasks"):
|
||||||
|
return
|
||||||
# Drop the FK constraint on tasks.contact_id
|
# Drop the FK constraint on tasks.contact_id
|
||||||
op.drop_constraint("tasks_contact_id_fkey", "tasks", type_="foreignkey")
|
op.drop_constraint("tasks_contact_id_fkey", "tasks", type_="foreignkey")
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ Revises: 0128
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
revision = "0129"
|
revision = "0129"
|
||||||
down_revision = "0128"
|
down_revision = "0128"
|
||||||
@@ -27,8 +28,25 @@ TABLES_NEEDING_RLS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _table_exists(conn, table_name: str) -> bool:
|
||||||
|
"""True when the table exists (dual-path convergence, Gate B).
|
||||||
|
|
||||||
|
Plugin-owned tables may not exist yet on a fresh install when Alembic
|
||||||
|
reaches this revision — skip them instead of failing. The plugin-side
|
||||||
|
convergence migrations apply the same RLS policies.
|
||||||
|
"""
|
||||||
|
row = conn.execute(
|
||||||
|
sa.text("SELECT to_regclass(:tname) IS NOT NULL"),
|
||||||
|
{"tname": f"public.{table_name}"},
|
||||||
|
).scalar()
|
||||||
|
return bool(row)
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
for table in TABLES_NEEDING_RLS:
|
for table in TABLES_NEEDING_RLS:
|
||||||
|
if not _table_exists(conn, table):
|
||||||
|
continue
|
||||||
op.execute(f"ALTER TABLE {table} ENABLE ROW LEVEL SECURITY;")
|
op.execute(f"ALTER TABLE {table} ENABLE ROW LEVEL SECURITY;")
|
||||||
op.execute(
|
op.execute(
|
||||||
f"CREATE POLICY tenant_isolation ON {table} "
|
f"CREATE POLICY tenant_isolation ON {table} "
|
||||||
@@ -37,6 +55,9 @@ def upgrade() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
for table in TABLES_NEEDING_RLS:
|
for table in TABLES_NEEDING_RLS:
|
||||||
|
if not _table_exists(conn, table):
|
||||||
|
continue
|
||||||
op.execute(f"DROP POLICY IF EXISTS tenant_isolation ON {table};")
|
op.execute(f"DROP POLICY IF EXISTS tenant_isolation ON {table};")
|
||||||
op.execute(f"ALTER TABLE {table} DISABLE ROW LEVEL SECURITY;")
|
op.execute(f"ALTER TABLE {table} DISABLE ROW LEVEL SECURITY;")
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ Revises: 0135
|
|||||||
Create Date: 2026-08-21
|
Create Date: 2026-08-21
|
||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
revision = "0136"
|
revision = "0136"
|
||||||
down_revision = "0135"
|
down_revision = "0135"
|
||||||
@@ -29,8 +30,25 @@ TABLES_WITH_BAD_RLS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _table_exists(conn, table_name: str) -> bool:
|
||||||
|
"""True when the table exists (dual-path convergence, Gate B).
|
||||||
|
|
||||||
|
Plugin-owned tables may not exist yet on a fresh install when Alembic
|
||||||
|
reaches this revision — skip them instead of failing. The plugin-side
|
||||||
|
convergence migrations apply the same RLS policies.
|
||||||
|
"""
|
||||||
|
row = conn.execute(
|
||||||
|
sa.text("SELECT to_regclass(:tname) IS NOT NULL"),
|
||||||
|
{"tname": f"public.{table_name}"},
|
||||||
|
).scalar()
|
||||||
|
return bool(row)
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
for table in TABLES_WITH_BAD_RLS:
|
for table in TABLES_WITH_BAD_RLS:
|
||||||
|
if not _table_exists(conn, table):
|
||||||
|
continue
|
||||||
# Drop old policy with app.tenant_id
|
# Drop old policy with app.tenant_id
|
||||||
op.execute(f"DROP POLICY IF EXISTS tenant_isolation ON {table};")
|
op.execute(f"DROP POLICY IF EXISTS tenant_isolation ON {table};")
|
||||||
# Create new policy with app.current_tenant_id
|
# Create new policy with app.current_tenant_id
|
||||||
@@ -41,7 +59,10 @@ def upgrade() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
for table in TABLES_WITH_BAD_RLS:
|
for table in TABLES_WITH_BAD_RLS:
|
||||||
|
if not _table_exists(conn, table):
|
||||||
|
continue
|
||||||
op.execute(f"DROP POLICY IF EXISTS tenant_isolation ON {table};")
|
op.execute(f"DROP POLICY IF EXISTS tenant_isolation ON {table};")
|
||||||
op.execute(
|
op.execute(
|
||||||
f"CREATE POLICY tenant_isolation ON {table} "
|
f"CREATE POLICY tenant_isolation ON {table} "
|
||||||
|
|||||||
@@ -19,7 +19,25 @@ branch_labels = None
|
|||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def _table_exists(conn, table_name: str) -> bool:
|
||||||
|
"""True when the table exists (dual-path convergence, Gate B).
|
||||||
|
|
||||||
|
On a fresh install the tags plugin SQL migration has not run yet when
|
||||||
|
Alembic reaches this revision — skip instead of failing. The plugin-side
|
||||||
|
convergence migration adds the same columns.
|
||||||
|
"""
|
||||||
|
row = conn.execute(
|
||||||
|
sa.text("SELECT to_regclass(:tname) IS NOT NULL"),
|
||||||
|
{"tname": f"public.{table_name}"},
|
||||||
|
).scalar()
|
||||||
|
return bool(row)
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
if not _table_exists(conn, "tags"):
|
||||||
|
return
|
||||||
|
|
||||||
# parent_id for tree structure (self-referencing FK)
|
# parent_id for tree structure (self-referencing FK)
|
||||||
op.add_column("tags", sa.Column("parent_id", PGUUID(as_uuid=True), nullable=True))
|
op.add_column("tags", sa.Column("parent_id", PGUUID(as_uuid=True), nullable=True))
|
||||||
op.create_foreign_key(
|
op.create_foreign_key(
|
||||||
@@ -35,6 +53,9 @@ def upgrade() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
if not _table_exists(conn, "tags"):
|
||||||
|
return
|
||||||
op.drop_column("tags", "icon")
|
op.drop_column("tags", "icon")
|
||||||
op.drop_column("tags", "applicable_to")
|
op.drop_column("tags", "applicable_to")
|
||||||
op.drop_index("ix_tags_parent", table_name="tags")
|
op.drop_index("ix_tags_parent", table_name="tags")
|
||||||
|
|||||||
@@ -18,11 +18,31 @@ branch_labels = None
|
|||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def _table_exists(conn, table_name: str) -> bool:
|
||||||
|
"""True when the table exists (dual-path convergence, Gate B).
|
||||||
|
|
||||||
|
On a fresh install the report_generator plugin SQL migration has not
|
||||||
|
run yet when Alembic reaches this revision — skip instead of failing.
|
||||||
|
The plugin-side convergence migration adds the same column.
|
||||||
|
"""
|
||||||
|
row = conn.execute(
|
||||||
|
sa.text("SELECT to_regclass(:tname) IS NOT NULL"),
|
||||||
|
{"tname": f"public.{table_name}"},
|
||||||
|
).scalar()
|
||||||
|
return bool(row)
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
if not _table_exists(conn, "report_templates"):
|
||||||
|
return
|
||||||
op.add_column("report_templates", sa.Column("folder_id", PGUUID(as_uuid=True), nullable=True))
|
op.add_column("report_templates", sa.Column("folder_id", PGUUID(as_uuid=True), nullable=True))
|
||||||
op.create_index("ix_report_templates_folder", "report_templates", ["folder_id"])
|
op.create_index("ix_report_templates_folder", "report_templates", ["folder_id"])
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
if not _table_exists(conn, "report_templates"):
|
||||||
|
return
|
||||||
op.drop_index("ix_report_templates_folder", table_name="report_templates")
|
op.drop_index("ix_report_templates_folder", table_name="report_templates")
|
||||||
op.drop_column("report_templates", "folder_id")
|
op.drop_column("report_templates", "folder_id")
|
||||||
|
|||||||
@@ -18,11 +18,31 @@ branch_labels = None
|
|||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def _table_exists(conn, table_name: str) -> bool:
|
||||||
|
"""True when the table exists (dual-path convergence, Gate B).
|
||||||
|
|
||||||
|
On a fresh install the kommunikation plugin SQL migration has not run
|
||||||
|
yet when Alembic reaches this revision — skip instead of failing.
|
||||||
|
The plugin-side migration adds the same column idempotently.
|
||||||
|
"""
|
||||||
|
row = conn.execute(
|
||||||
|
sa.text("SELECT to_regclass(:tname) IS NOT NULL"),
|
||||||
|
{"tname": f"public.{table_name}"},
|
||||||
|
).scalar()
|
||||||
|
return bool(row)
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
if not _table_exists(conn, "comm_conversations"):
|
||||||
|
return
|
||||||
op.add_column("comm_conversations", sa.Column("folder_id", PGUUID(as_uuid=True), nullable=True))
|
op.add_column("comm_conversations", sa.Column("folder_id", PGUUID(as_uuid=True), nullable=True))
|
||||||
op.create_index("ix_comm_conversations_folder", "comm_conversations", ["folder_id"])
|
op.create_index("ix_comm_conversations_folder", "comm_conversations", ["folder_id"])
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
if not _table_exists(conn, "comm_conversations"):
|
||||||
|
return
|
||||||
op.drop_index("ix_comm_conversations_folder", table_name="comm_conversations")
|
op.drop_index("ix_comm_conversations_folder", table_name="comm_conversations")
|
||||||
op.drop_column("comm_conversations", "folder_id")
|
op.drop_column("comm_conversations", "folder_id")
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- Dual-path convergence (Gate B): add compliance columns that Alembic
|
||||||
|
-- migration 0119 adds on the core path. Idempotent so both install paths
|
||||||
|
-- converge to the identical schema.
|
||||||
|
ALTER TABLE ai_providers ADD COLUMN IF NOT EXISTS region VARCHAR(20) NOT NULL DEFAULT 'unknown';
|
||||||
|
ALTER TABLE ai_providers ADD COLUMN IF NOT EXISTS hosting_type VARCHAR(30) NOT NULL DEFAULT 'cloud';
|
||||||
|
ALTER TABLE ai_providers ADD COLUMN IF NOT EXISTS dpa_status VARCHAR(20) NOT NULL DEFAULT 'none';
|
||||||
|
ALTER TABLE ai_providers ADD COLUMN IF NOT EXISTS retention_policy TEXT NOT NULL DEFAULT '';
|
||||||
|
ALTER TABLE ai_providers ADD COLUMN IF NOT EXISTS training_on_customer_data BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
ALTER TABLE ai_providers ADD COLUMN IF NOT EXISTS transfer_notice TEXT NOT NULL DEFAULT '';
|
||||||
|
ALTER TABLE ai_providers ADD COLUMN IF NOT EXISTS allowed_data_classes JSONB NOT NULL DEFAULT '[]'::jsonb;
|
||||||
@@ -42,7 +42,7 @@ class AIAssistantPlugin(BasePlugin):
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
events=[],
|
events=[],
|
||||||
migrations=["0001_initial.sql", "0002_folders_attachments.sql"],
|
migrations=["0001_initial.sql", "0002_folders_attachments.sql", "0003_sort_order.sql", "0004_compliance_fields.sql"],
|
||||||
permissions=[
|
permissions=[
|
||||||
"ai:read",
|
"ai:read",
|
||||||
"ai:write",
|
"ai:write",
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
-- Dual-path convergence (Gate B): create the ReAct step-tracking table
|
||||||
|
-- that Alembic migration 0121 creates on the core path, add the Phase-F
|
||||||
|
-- columns from 0122, and apply the RLS policy from 0129/0136. Idempotent
|
||||||
|
-- so both install paths converge to the identical schema.
|
||||||
|
CREATE TABLE IF NOT EXISTS automation_agent_run_steps (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
tenant_id UUID NOT NULL,
|
||||||
|
agent_run_id UUID NOT NULL REFERENCES automation_agent_runs(id) ON DELETE CASCADE,
|
||||||
|
step_number INTEGER NOT NULL,
|
||||||
|
thought TEXT,
|
||||||
|
action VARCHAR(255),
|
||||||
|
action_input JSONB,
|
||||||
|
observation TEXT,
|
||||||
|
cost_usd FLOAT NOT NULL DEFAULT 0.0,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_agent_run_steps_run ON automation_agent_run_steps(tenant_id, agent_run_id);
|
||||||
|
|
||||||
|
ALTER TABLE automation_agent_definitions ADD COLUMN IF NOT EXISTS temperature FLOAT NOT NULL DEFAULT 0.3;
|
||||||
|
ALTER TABLE automation_agent_definitions ADD COLUMN IF NOT EXISTS max_tokens INTEGER NOT NULL DEFAULT 1000;
|
||||||
|
ALTER TABLE automation_agent_definitions ADD COLUMN IF NOT EXISTS max_steps INTEGER NOT NULL DEFAULT 20;
|
||||||
|
ALTER TABLE automation_agent_definitions ADD COLUMN IF NOT EXISTS trace_mode VARCHAR(20) NOT NULL DEFAULT 'standard';
|
||||||
|
ALTER TABLE automation_agent_definitions ADD COLUMN IF NOT EXISTS skill_ids JSONB NOT NULL DEFAULT '[]'::jsonb;
|
||||||
|
ALTER TABLE automation_agent_definitions ADD COLUMN IF NOT EXISTS trigger_config JSONB NOT NULL DEFAULT '{}'::jsonb;
|
||||||
|
ALTER TABLE automation_agent_definitions ADD COLUMN IF NOT EXISTS ai_use_case_metadata JSONB NOT NULL DEFAULT '{}'::jsonb;
|
||||||
|
|
||||||
|
-- RLS matching migrations 0129 + 0136 (current_tenant_id variant)
|
||||||
|
ALTER TABLE automation_agent_run_steps ENABLE ROW LEVEL SECURITY;
|
||||||
|
DROP POLICY IF EXISTS tenant_isolation ON automation_agent_run_steps;
|
||||||
|
CREATE POLICY tenant_isolation ON automation_agent_run_steps
|
||||||
|
USING (tenant_id::text = current_setting('app.current_tenant_id', true));
|
||||||
@@ -62,7 +62,7 @@ class AutomationPlugin(BasePlugin):
|
|||||||
"mail.received",
|
"mail.received",
|
||||||
"workflow.timeout",
|
"workflow.timeout",
|
||||||
],
|
],
|
||||||
migrations=["0001_initial.sql", "0002_agent_subtasks.sql", "0003_skill_definitions.sql"],
|
migrations=["0001_initial.sql", "0002_agent_subtasks.sql", "0003_skill_definitions.sql", "0004_run_steps_phase_f.sql"],
|
||||||
permissions=[
|
permissions=[
|
||||||
"automation:read",
|
"automation:read",
|
||||||
"automation:write",
|
"automation:write",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class KnowledgePlugin(BasePlugin):
|
|||||||
"""Register event-driven extraction hooks on activation."""
|
"""Register event-driven extraction hooks on activation."""
|
||||||
await super().on_activate(db, service_container, event_bus)
|
await super().on_activate(db, service_container, event_bus)
|
||||||
try:
|
try:
|
||||||
from app.core.hooks import register_action
|
from app.core.hooks import get_hook_registry
|
||||||
from app.plugins.builtins.knowledge.services import extract_knowledge
|
from app.plugins.builtins.knowledge.services import extract_knowledge
|
||||||
async def on_wiki_create(*args, **kwargs):
|
async def on_wiki_create(*args, **kwargs):
|
||||||
article_id = kwargs.get("article_id") or kwargs.get("entity_id")
|
article_id = kwargs.get("article_id") or kwargs.get("entity_id")
|
||||||
@@ -41,7 +41,9 @@ class KnowledgePlugin(BasePlugin):
|
|||||||
source_type="wiki_article", source_id=uuid.UUID(str(article_id)),
|
source_type="wiki_article", source_id=uuid.UUID(str(article_id)),
|
||||||
source_title=title, source_text=content,
|
source_title=title, source_text=content,
|
||||||
)
|
)
|
||||||
register_action("wiki.article.created", on_wiki_create, priority=20, owner_tag="knowledge")
|
get_hook_registry().register_action(
|
||||||
|
"wiki.article.created", on_wiki_create, priority=20, owner_tag="knowledge"
|
||||||
|
)
|
||||||
# H-DATA-LIFE: Re-extract when wiki article is updated
|
# H-DATA-LIFE: Re-extract when wiki article is updated
|
||||||
async def on_wiki_update(*args, **kwargs):
|
async def on_wiki_update(*args, **kwargs):
|
||||||
article_id = kwargs.get("article_id") or kwargs.get("entity_id")
|
article_id = kwargs.get("article_id") or kwargs.get("entity_id")
|
||||||
@@ -57,7 +59,9 @@ class KnowledgePlugin(BasePlugin):
|
|||||||
source_type="wiki_article", source_id=uuid.UUID(str(article_id)),
|
source_type="wiki_article", source_id=uuid.UUID(str(article_id)),
|
||||||
source_title=title, source_text=content,
|
source_title=title, source_text=content,
|
||||||
)
|
)
|
||||||
register_action("wiki.article.updated", on_wiki_update, priority=20, owner_tag="knowledge")
|
get_hook_registry().register_action(
|
||||||
|
"wiki.article.updated", on_wiki_update, priority=20, owner_tag="knowledge"
|
||||||
|
)
|
||||||
logger.info("Registered knowledge extraction hooks")
|
logger.info("Registered knowledge extraction hooks")
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to register knowledge hooks")
|
logger.exception("Failed to register knowledge hooks")
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- Dual-path convergence (Gate B): add columns that Alembic migrations
|
||||||
|
-- 0120 (is_system) and 0140 (folder_id) add on the core path. Idempotent
|
||||||
|
-- so both install paths converge to the identical schema.
|
||||||
|
ALTER TABLE comm_conversations ADD COLUMN IF NOT EXISTS is_system BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_comm_conversations_tenant_system ON comm_conversations(tenant_id, is_system);
|
||||||
|
ALTER TABLE comm_conversations ADD COLUMN IF NOT EXISTS folder_id UUID;
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_comm_conversations_folder ON comm_conversations(folder_id);
|
||||||
@@ -39,7 +39,7 @@ class KommunikationPlugin(BasePlugin):
|
|||||||
"participant.left",
|
"participant.left",
|
||||||
"reaction.added",
|
"reaction.added",
|
||||||
],
|
],
|
||||||
migrations=["0001_initial.sql"],
|
migrations=["0001_initial.sql", "0002_system_channel_folders.sql"],
|
||||||
permissions=[
|
permissions=[
|
||||||
"comm:read",
|
"comm:read",
|
||||||
"comm:write",
|
"comm:write",
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- Dual-path convergence (Gate B): add the folder_id column that Alembic
|
||||||
|
-- migration 0139 adds on the core path. Idempotent so both install paths
|
||||||
|
-- converge to the identical schema.
|
||||||
|
ALTER TABLE report_templates ADD COLUMN IF NOT EXISTS folder_id UUID;
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_report_templates_folder ON report_templates(folder_id);
|
||||||
@@ -24,7 +24,7 @@ class ReportGeneratorPlugin(BasePlugin):
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
events=["report.requested", "report.generated"],
|
events=["report.requested", "report.generated"],
|
||||||
migrations=["0001_initial.sql"],
|
migrations=["0001_initial.sql", "0002_reports_folder_id.sql"],
|
||||||
permissions=["reports:read", "reports:generate", "reports:manage_templates"],
|
permissions=["reports:read", "reports:generate", "reports:manage_templates"],
|
||||||
menu_items=[
|
menu_items=[
|
||||||
FrontendMenuItem(label_key='nav.reports', label='Berichte', path='/reports', icon='BarChart3', order=70),
|
FrontendMenuItem(label_key='nav.reports', label='Berichte', path='/reports', icon='BarChart3', order=70),
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- Dual-path convergence (Gate B): apply the tag tree structure that
|
||||||
|
-- Alembic migration 0138 adds on the core path. Idempotent so both install
|
||||||
|
-- paths converge to the identical schema.
|
||||||
|
ALTER TABLE tags ADD COLUMN IF NOT EXISTS parent_id UUID;
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_tags_parent ON tags(parent_id);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_tags_parent_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE tags ADD CONSTRAINT fk_tags_parent_id
|
||||||
|
FOREIGN KEY (parent_id) REFERENCES tags(id) ON DELETE SET NULL;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
ALTER TABLE tags ADD COLUMN IF NOT EXISTS applicable_to JSONB;
|
||||||
|
ALTER TABLE tags ADD COLUMN IF NOT EXISTS icon VARCHAR(50);
|
||||||
@@ -23,7 +23,7 @@ class TagsPlugin(BasePlugin):
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
events=[],
|
events=[],
|
||||||
migrations=["0001_initial.sql", "0002_add_deleted_at.sql"],
|
migrations=["0001_initial.sql", "0002_add_deleted_at.sql", "0003_tree_structure.sql"],
|
||||||
permissions=[
|
permissions=[
|
||||||
"tags:read",
|
"tags:read",
|
||||||
"tags:write",
|
"tags:write",
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
-- Dual-path convergence (Gate B): apply the unified task system that
|
||||||
|
-- Alembic migrations 0124 (columns/indexes/FK) and 0127 (drop legacy FK)
|
||||||
|
-- create on the core path. Idempotent so both install paths converge to
|
||||||
|
-- the identical schema. The end state has NO foreign key on contact_id.
|
||||||
|
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS assignee_type VARCHAR(20) NOT NULL DEFAULT 'user';
|
||||||
|
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS assignee_id UUID;
|
||||||
|
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS entity_type VARCHAR(80);
|
||||||
|
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS entity_id UUID;
|
||||||
|
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS creator_type VARCHAR(20) NOT NULL DEFAULT 'user';
|
||||||
|
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS creator_id UUID;
|
||||||
|
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS parent_task_id UUID;
|
||||||
|
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS depends_on JSONB NOT NULL DEFAULT '[]'::jsonb;
|
||||||
|
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS task_type VARCHAR(30) NOT NULL DEFAULT 'todo';
|
||||||
|
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS success_criteria JSONB;
|
||||||
|
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS target_date TIMESTAMPTZ;
|
||||||
|
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS progress INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_tasks_tenant_entity ON tasks(tenant_id, entity_type, entity_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_tasks_tenant_assignee ON tasks(tenant_id, assignee_type, assignee_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_tasks_tenant_parent ON tasks(tenant_id, parent_task_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_tasks_tenant_type ON tasks(tenant_id, task_type);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_tasks_parent_task_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE tasks ADD CONSTRAINT fk_tasks_parent_task_id
|
||||||
|
FOREIGN KEY (parent_task_id) REFERENCES tasks(id) ON DELETE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
@@ -29,7 +29,7 @@ class TasksPlugin(BasePlugin):
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
events=[],
|
events=[],
|
||||||
migrations=["0001_initial.sql"],
|
migrations=["0001_initial.sql", "0002_unified_task_system.sql"],
|
||||||
permissions=[
|
permissions=[
|
||||||
"tasks:read",
|
"tasks:read",
|
||||||
"tasks:write",
|
"tasks:write",
|
||||||
|
|||||||
Reference in New Issue
Block a user