From 83857507ea0af454a71837d60a45c67cd14e45d5 Mon Sep 17 00:00:00 2001 From: Sergey Mankovsky Date: Wed, 25 Sep 2024 13:13:18 +0200 Subject: [PATCH] Make sure room names are unique --- server/migrations/env.py | 13 +++---- .../0925da921477_unique_room_names.py | 34 ++++++++++++++++++ server/reflector/db/rooms.py | 14 ++++++-- www/app/(app)/rooms/page.tsx | 36 +++++++++++-------- 4 files changed, 74 insertions(+), 23 deletions(-) create mode 100644 server/migrations/versions/0925da921477_unique_room_names.py diff --git a/server/migrations/env.py b/server/migrations/env.py index 3c893c01..226b95b5 100644 --- a/server/migrations/env.py +++ b/server/migrations/env.py @@ -1,11 +1,9 @@ from logging.config import fileConfig -from reflector.settings import settings -from reflector.db import metadata - -from sqlalchemy import engine_from_config -from sqlalchemy import pool from alembic import context +from reflector.db import metadata +from reflector.settings import settings +from sqlalchemy import engine_from_config, pool # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -45,6 +43,7 @@ def run_migrations_offline() -> None: target_metadata=target_metadata, literal_binds=True, dialect_opts={"paramstyle": "named"}, + render_as_batch=True, ) with context.begin_transaction(): @@ -67,7 +66,9 @@ def run_migrations_online() -> None: ) with connectable.connect() as connection: - context.configure(connection=connection, target_metadata=target_metadata) + context.configure( + connection=connection, target_metadata=target_metadata, render_as_batch=True + ) with context.begin_transaction(): context.run_migrations() diff --git a/server/migrations/versions/0925da921477_unique_room_names.py b/server/migrations/versions/0925da921477_unique_room_names.py new file mode 100644 index 00000000..d9dbc194 --- /dev/null +++ b/server/migrations/versions/0925da921477_unique_room_names.py @@ -0,0 +1,34 @@ +"""Unique room names + +Revision ID: 0925da921477 +Revises: 764ce6db4388 +Create Date: 2024-09-24 16:12:56.944133 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "0925da921477" +down_revision: Union[str, None] = "764ce6db4388" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("room", schema=None) as batch_op: + batch_op.create_unique_constraint("uq_room_name", ["name"]) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("room", schema=None) as batch_op: + batch_op.drop_constraint("uq_room_name", type_="unique") + + # ### end Alembic commands ### diff --git a/server/reflector/db/rooms.py b/server/reflector/db/rooms.py index f8fef8ad..5dc09542 100644 --- a/server/reflector/db/rooms.py +++ b/server/reflector/db/rooms.py @@ -1,4 +1,5 @@ from datetime import datetime +from sqlite3 import IntegrityError from typing import Literal import sqlalchemy @@ -12,7 +13,7 @@ rooms = sqlalchemy.Table( "room", metadata, sqlalchemy.Column("id", sqlalchemy.String, primary_key=True), - sqlalchemy.Column("name", sqlalchemy.String, nullable=False), + sqlalchemy.Column("name", sqlalchemy.String, nullable=False, unique=True), sqlalchemy.Column("user_id", sqlalchemy.String, nullable=False), sqlalchemy.Column("created_at", sqlalchemy.DateTime, nullable=False), sqlalchemy.Column( @@ -113,7 +114,10 @@ class RoomController: recording_trigger=recording_trigger, ) query = rooms.insert().values(**room.model_dump()) - await database.execute(query) + try: + await database.execute(query) + except IntegrityError: + raise HTTPException(status_code=400, detail="Room name is not unique") return room async def update(self, room: Room, values: dict, mutate=True): @@ -121,7 +125,11 @@ class RoomController: Update a room fields with key/values in values """ query = rooms.update().where(rooms.c.id == room.id).values(**values) - await database.execute(query) + try: + await database.execute(query) + except IntegrityError: + raise HTTPException(status_code=400, detail="Room name is not unique") + if mutate: for key, value in values.items(): setattr(room, key, value) diff --git a/www/app/(app)/rooms/page.tsx b/www/app/(app)/rooms/page.tsx index 0c71d6d6..6de2778e 100644 --- a/www/app/(app)/rooms/page.tsx +++ b/www/app/(app)/rooms/page.tsx @@ -36,11 +36,7 @@ import { FaEllipsisVertical, FaTrash, FaPencil, FaLink } from "react-icons/fa6"; import useApi from "../../lib/useApi"; import useRoomList from "./useRoomList"; import { Select, Options, OptionBase } from "chakra-react-select"; - -interface Stream { - stream_id: number; - name: string; -} +import { ApiError } from "../../api"; interface SelectOption extends OptionBase { label: string; @@ -81,8 +77,7 @@ export default function RoomsList() { const { loading, response, refetch } = useRoomList(page); const [streams, setStreams] = useState([]); const [topics, setTopics] = useState([]); - - const [error, setError] = useState(""); + const [nameError, setNameError] = useState(""); const [linkCopied, setLinkCopied] = useState(""); interface Stream { stream_id: number; @@ -151,7 +146,7 @@ export default function RoomsList() { const handleSaveRoom = async () => { try { if (RESERVED_PATHS.includes(room.name)) { - setError("This room name is reserved. Please choose another name."); + setNameError("This room name is reserved. Please choose another name."); return; } @@ -180,12 +175,24 @@ export default function RoomsList() { setRoom(roomInitialState); setIsEditing(false); setEditRoomId(""); - setError(""); + setNameError(""); refetch(); + onClose(); } catch (err) { - console.error(err); + if (err instanceof ApiError) { + const apiError = err as ApiError; + if ( + apiError.status === 400 && + (apiError.body as any).detail == "Room name is not unique" + ) { + setNameError( + "This room name is already taken. Please choose a different name.", + ); + } else { + setNameError("An error occurred. Please try again."); + } + } } - onClose(); }; const handleEditRoom = (roomId, roomData) => { @@ -201,6 +208,7 @@ export default function RoomsList() { }); setEditRoomId(roomId); setIsEditing(true); + setNameError(""); onOpen(); }; @@ -222,7 +230,7 @@ export default function RoomsList() { .replace(/[^a-zA-Z0-9\s-]/g, "") .replace(/\s+/g, "-") .toLowerCase(); - setError(""); + setNameError(""); } setRoom({ ...room, @@ -254,7 +262,7 @@ export default function RoomsList() { onClick={() => { setIsEditing(false); setRoom(roomInitialState); - setError(""); + setNameError(""); onOpen(); }} > @@ -277,7 +285,7 @@ export default function RoomsList() { No spaces or special characters allowed - {error && {error}} + {nameError && {nameError}}