"""A template's blueprint: parsing it, and keeping its id unique. The config id is a slug, not a row id — it is what documents record as their origin and what the catalog matches on, so two rows must never share one. Three endpoints need that rule (add from catalog, save from the builder, duplicate), which is why it is written once here. """ import uuid from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.authoring.schema import AuthoringTemplate from app.errors import ApiError from app.models import Template from app.template_import import TemplateImportError, parse_template def parse_or_422(source: str) -> AuthoringTemplate: """YAML in, validated blueprint out. The parse error is the message: it already says which field is wrong, and an admin is the one reading it.""" try: return parse_template(source) except TemplateImportError as exc: raise ApiError(422, str(exc), "invalid_template") from None async def taken_config_ids(db: AsyncSession) -> set[str]: return set((await db.execute(select(Template.config["id"].astext))).scalars().all()) def unique_config_id(base: str, taken: set[str], *, suffix: str = "") -> str: """`base`, or `base-2`, `base-3` … until it is free. `suffix` marks derived ids (a duplicate becomes `base-kopie`), so a copy reads as a copy in the one place ids are visible. """ stem = f"{base}{suffix}" candidate = stem counter = 2 while candidate in taken: candidate = f"{stem}-{counter}" counter += 1 return candidate async def ensure_config_id_free( db: AsyncSession, config_id: str, *, except_row: uuid.UUID ) -> None: """Refuse an edit that would move a config id onto a DIFFERENT row. Editing keeps the id stable, so a clash is never the row's own id — it means someone would silently steal another template's identity. """ clash = ( await db.execute( select(Template.id).where( Template.config["id"].astext == config_id, Template.id != except_row, ) ) ).scalar_one_or_none() if clash is not None: raise ApiError( 409, f"Another template already uses the id '{config_id}'.", "id_taken" )