feat: canonical flow directory with sync engine (files are source of truth)

This commit is contained in:
Jonas Miederer 2026-09-07 08:59:56 +00:00
parent 7d5ade6b6a
commit c65e3ed332
16 changed files with 4167 additions and 42 deletions

View file

@ -56,7 +56,82 @@ def list_definitions(session: Session) -> list[FlowDefinition]:
return list(session.exec(select(FlowDefinition).order_by(FlowDefinition.name)).all())
def _validate_flow_json(flow_json: dict) -> None:
def get_definition_by_source_path(
session: Session, source_path: str
) -> FlowDefinition | None:
"""Find the definition synced from a given backend/flows filename."""
return session.exec(
select(FlowDefinition).where(FlowDefinition.source_path == source_path)
).first()
def set_definition_name(
session: Session, definition: FlowDefinition, name: str
) -> FlowDefinition:
"""Rename a definition and record its source file. Name clashes 409."""
definition.name = name
session.add(definition)
try:
session.commit()
except IntegrityError as exc:
session.rollback()
raise ConflictError(f"flow {name!r} already exists") from exc
session.refresh(definition)
return definition
def next_version_no(session: Session, flow_id: UUID) -> int:
max_no = session.exec(
select(func.max(FlowVersion.version_no)).where(FlowVersion.flow_id == flow_id)
).one()
return (max_no or 0) + 1
def delete_definition_cascade(
session: Session, definition: FlowDefinition
) -> dict[str, int]:
"""Remove a definition and its whole subtree (tasks, runs, versions).
Used when the canonical file disappears: the directory is the source of
truth, so the derived registry follows. Commits once.
"""
version_ids = list(
session.exec(
select(FlowVersion.id).where(FlowVersion.flow_id == definition.id)
).all()
)
task_count = 0
run_count = 0
if version_ids:
tasks = list(
session.exec(
select(ApprovalTask).where(ApprovalTask.version_id.in_(version_ids))
).all()
)
runs = list(
session.exec(
select(FlowRun).where(FlowRun.version_id.in_(version_ids))
).all()
)
task_count, run_count = len(tasks), len(runs)
for row in (*tasks, *runs):
session.delete(row)
for version in session.exec(
select(FlowVersion).where(FlowVersion.flow_id == definition.id)
).all():
session.delete(version)
session.delete(definition)
session.commit()
return {
"definitions": 1,
"versions": len(version_ids),
"runs": run_count,
"tasks": task_count,
}
def validate_flow_json(flow_json: dict) -> None:
"""Reject anything that is not a Langflow-export-shaped document."""
if not isinstance(flow_json, dict):
raise InvalidError("langflow_json must be an object")
data = flow_json.get("data")
@ -70,7 +145,7 @@ def create_version(
session: Session, flow_id: UUID, data: FlowVersionCreate
) -> FlowVersion:
get_definition(session, flow_id)
_validate_flow_json(data.langflow_json)
validate_flow_json(data.langflow_json)
max_no = session.exec(
select(func.max(FlowVersion.version_no)).where(FlowVersion.flow_id == flow_id)
).one()