Pablan, as it stands
Self-hosted knowledge management for SMEs: a split-screen Markdown editor whose sections an LLM refines while you write, and RAG question answering over the documents that result. FastAPI + Postgres/pgvector on the back, SvelteKit on the front, everything OpenAI-compatible and self-hostable. Squashed into a single commit; the development history stays local. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CA43ZJda8Rbp2hKXNy8f6b
This commit is contained in:
co-authored by
Claude Opus 5
parent
68d3a43191
commit
784b76baf7
@@ -0,0 +1,67 @@
|
||||
"""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"
|
||||
)
|
||||
Reference in New Issue
Block a user