fix: 4 API bugs found by integration tests
Check Cross-Plugin Imports / check (push) Has been cancelled

1. tags.owner_id column missing — Migration 0105 adds owner_id to tags table
2. contacts trigger first_name vs firstname — Migration 0105 recreates
   unified_search TSV trigger with correct column names (firstname, surname, etc.)
3. create_webhook() missing is_system_admin param — Add to webhook_service.py
4. Missing GET /api/v1/search endpoint — Add to unified_search/routes.py
   with shared _do_search() helper for GET+POST

Alembic head: 0104 → 0105
This commit is contained in:
Agent Zero
2026-08-04 22:57:37 +02:00
parent b115d8211e
commit f15c3bec46
3 changed files with 182 additions and 4 deletions
@@ -0,0 +1,153 @@
"""Fix tags.owner_id missing column and contacts_tsv_trigger column mismatch.
Bug 1: tags.owner_id — Migration 0102 tried to add owner_id to tags but only
if the table existed at that point. If the tags table was created later (by
plugin migration), owner_id was never added. This migration ensures owner_id
exists on the tags table.
Bug 2: contacts_tsv_trigger — The unified_search plugin migration 0001 created
a trigger function referencing NEW.first_name, NEW.last_name, NEW.email,
NEW.phone, NEW.mobile, NEW.notes. After migration 0021 unified contacts,
the columns are named firstname, surname, email_1, phone_1, phone_2,
projectnote. The trigger must be recreated with correct column names.
Revision ID: 0105
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID as PGUUID
revision = "0105"
down_revision = "0104"
branch_labels = None
depends_on = None
def _table_exists(conn, table_name: str) -> bool:
result = conn.execute(
sa.text(
"SELECT EXISTS (SELECT 1 FROM information_schema.tables "
"WHERE table_name = :name)"
),
{"name": table_name},
)
return result.scalar()
def _column_exists(conn, table_name: str, column_name: str) -> bool:
result = conn.execute(
sa.text(
"SELECT EXISTS (SELECT 1 FROM information_schema.columns "
"WHERE table_name = :table AND column_name = :col)"
),
{"table": table_name, "col": column_name},
)
return result.scalar()
def upgrade() -> None:
conn = op.get_bind()
# ── Bug 1: Add owner_id to tags table if missing ──
if _table_exists(conn, "tags"):
if not _column_exists(conn, "tags", "owner_id"):
op.add_column(
"tags",
sa.Column(
"owner_id",
PGUUID(as_uuid=True),
sa.ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
),
)
op.create_index(
"ix_tags_owner_id",
"tags",
["owner_id"],
)
print("[0105] Added owner_id to tags table")
else:
print("[0105] tags.owner_id already exists — skipping")
else:
print("[0105] tags table does not exist — skipping")
# ── Bug 2: Recreate contacts_tsv_trigger with correct column names ──
if _table_exists(conn, "contacts"):
# Drop old trigger and function
op.execute("DROP TRIGGER IF EXISTS contacts_tsv_update ON contacts")
op.execute("DROP FUNCTION IF EXISTS contacts_tsv_trigger()")
# Recreate trigger function with current column names
# Contacts table after migration 0021 uses: firstname, surname, email_1,
# email_2, phone_1, phone_2, name, displayname, code, mailing_city,
# mailing_postalcode, tags, projectnote
op.execute(
"""
CREATE OR REPLACE FUNCTION contacts_tsv_trigger() RETURNS trigger AS $$
BEGIN
NEW.search_tsv :=
setweight(to_tsvector('pg_catalog.german',
coalesce(NEW.name, '') || ' ' || coalesce(NEW.displayname, '') ||
' ' || coalesce(NEW.firstname, '') || ' ' || coalesce(NEW.surname, '')), 'A') ||
setweight(to_tsvector('pg_catalog.german',
coalesce(NEW.email_1, '') || ' ' || coalesce(NEW.email_2, '')), 'B') ||
setweight(to_tsvector('pg_catalog.german',
coalesce(NEW.phone_1, '') || ' ' || coalesce(NEW.phone_2, '')), 'C') ||
setweight(to_tsvector('pg_catalog.german',
coalesce(NEW.code, '') || ' ' || coalesce(NEW.mailing_city, '') ||
' ' || coalesce(NEW.mailing_postalcode, '') || ' ' || coalesce(NEW.tags, '') ||
' ' || coalesce(NEW.projectnote, '')), 'D');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
"""
)
# Recreate trigger
op.execute(
"""
CREATE TRIGGER contacts_tsv_update
BEFORE INSERT OR UPDATE ON contacts
FOR EACH ROW EXECUTE FUNCTION contacts_tsv_trigger();
"""
)
print("[0105] Recreated contacts_tsv_trigger with correct column names")
else:
print("[0105] contacts table does not exist — skipping trigger fix")
def downgrade() -> None:
conn = op.get_bind()
# Restore old trigger function (with incorrect column names for rollback)
if _table_exists(conn, "contacts"):
op.execute("DROP TRIGGER IF EXISTS contacts_tsv_update ON contacts")
op.execute("DROP FUNCTION IF EXISTS contacts_tsv_trigger()")
op.execute(
"""
CREATE OR REPLACE FUNCTION contacts_tsv_trigger() RETURNS trigger AS $$
BEGIN
NEW.search_tsv :=
setweight(to_tsvector('pg_catalog.german', coalesce(NEW.first_name, '') || ' ' || coalesce(NEW.last_name, '')), 'A') ||
setweight(to_tsvector('pg_catalog.german', coalesce(NEW.email, '')), 'B') ||
setweight(to_tsvector('pg_catalog.german', coalesce(NEW.phone, '') || ' ' || coalesce(NEW.mobile, '')), 'C') ||
setweight(to_tsvector('pg_catalog.german', coalesce(NEW.notes, '')), 'D');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
"""
)
op.execute(
"""
CREATE TRIGGER contacts_tsv_update
BEFORE INSERT OR UPDATE ON contacts
FOR EACH ROW EXECUTE FUNCTION contacts_tsv_trigger();
"""
)
# Remove owner_id from tags
if _table_exists(conn, "tags") and _column_exists(conn, "tags", "owner_id"):
op.drop_index("ix_tags_owner_id", table_name="tags")
op.drop_column("tags", "owner_id")
""