mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-19 23:01:29 +02:00
Creating the tag "Web Servers" and then "web-servers" was a 500. The
name-to-slug mapping is many-to-one, but uniqueness was checked on the
name while the database constrains the slug, so any pair of names
differing only in case, spaces or punctuation collided on insert.
generate_slug also dropped every character outside a-z0-9, so a name in
any non-Latin script reduced to the empty string. The first such tag
was created with an empty slug, which made it unaddressable — the row
could never be read, updated or deleted through /tags/{project}/{slug}
— and the second one collided with it and failed.
Three routes and two services each derived slugs their own way; two had
a uniqueness loop and three did not, and the loop was a check followed
by an insert, so eight concurrent requests produced four 500s.
There is now one helper. generate_slug keeps its exact behaviour for
Latin names and falls back to Unicode word characters when that yields
nothing, so 東京支社 and Москва get real slugs. add_with_unique_slug
picks a free suffix, counting rows pending in the session as taken so a
CSV import carrying "Ops Team" and "ops-team" in one request lands two
tags, then inserts inside a savepoint and retries when the database
reports a slug conflict — re-raising anything else so an unrelated
constraint still surfaces its own error. Tag creation gained the
duplicate-name guard organizations already had.
Verified: eight concurrent creates over colliding names across tags,
organizations and projects, three rounds, no 5xx and one row per name.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6LcH8uUddjJbKrahf34ys
40 lines
1.0 KiB
Python
40 lines
1.0 KiB
Python
import uuid
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from shared.models import Organization
|
|
from shared.utils.slug import add_with_unique_slug
|
|
from shared.utils.validation import clean_name
|
|
|
|
|
|
async def get_or_create_organization(
|
|
name: str,
|
|
project_id: uuid.UUID,
|
|
user_id: uuid.UUID,
|
|
session: AsyncSession,
|
|
description: str | None = None,
|
|
) -> Organization:
|
|
normalized_name = clean_name(name, max_len=100).lower()
|
|
|
|
result = await session.execute(
|
|
select(Organization).where(
|
|
Organization.name == normalized_name,
|
|
Organization.project_id == project_id,
|
|
)
|
|
)
|
|
organization = result.scalar_one_or_none()
|
|
|
|
if not organization:
|
|
organization = Organization(
|
|
name=normalized_name,
|
|
description=description,
|
|
project_id=project_id,
|
|
created_by=user_id,
|
|
)
|
|
await add_with_unique_slug(
|
|
session, organization, normalized_name, project_id=project_id
|
|
)
|
|
|
|
return organization
|