Retire a shared node in use with ?detach=true

DELETE /flows/library/{name} still answers 409 while nodes use the source.
With detach=true each of them keeps a copy of the code, in the published
flow and the draft, the source is deleted and the running users rebuild.
This commit is contained in:
2026-10-15 09:48:47 +02:00
parent 96f4fa5a38
commit 0004e4b9bd
7 changed files with 114 additions and 2 deletions
+17 -2
View File
@@ -336,8 +336,23 @@ def read_library(controller: FlowControllerDep) -> Any:
@router.delete("/library/{lib_name}", response_model=Message)
async def delete_shared_node(lib_name: str, controller: FlowControllerDep) -> Any:
"""Remove a shared source, as long as no flow still runs it."""
async def delete_shared_node(
lib_name: str, controller: FlowControllerDep, detach: bool = False
) -> Any:
"""Remove a shared source, as long as no flow still runs it.
With ``detach``, every node using it keeps a copy of the code as its own
first, so the source can be retired while flows still use it.
"""
if detach:
running = await run_in_threadpool(controller.store.usages, lib_name, False)
try:
await run_in_threadpool(controller.store.detach_lib, lib_name)
except LibNotFound:
raise HTTPException(status_code=404, detail=f"No shared node '{lib_name}'")
for flow in sorted({usage.split(".", 1)[0] for usage in running}):
await controller.reload_flow(flow)
return Message(message=f"Deleted shared node '{lib_name}'")
used_by = controller.store.usages(lib_name)
if used_by:
raise HTTPException(
+31
View File
@@ -400,6 +400,37 @@ class FlowStore:
self._set_source_ref(flow, node_id, None)
self._commit(f"Unshare node '{flow}.{node_id}'")
def detach_lib(self, name: str) -> None:
"""Give every node running this shared source a copy, then delete it.
Published flows and drafts alike, so a running flow keeps the same code
and nothing waits on a publish. Each document changed goes up a version,
so an editor holding the old one gets a conflict on its next save
instead of pointing a node back at a source that is gone.
"""
with self._write_lock:
code = self.read_lib_source(name)
for flow in self.list_flows():
for path, node_file in (
(self._flow_file(flow), self._node_file),
(self._draft_file(flow), self._draft_node_file),
):
if not path.exists():
continue
definition = FlowDef.model_validate_json(path.read_text())
users = [n for n in definition.nodes if n.source_ref == name]
if not users:
continue
for node in users:
target = node_file(flow, node.id)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(code)
node.source_ref = None
definition.version += 1
path.write_text(definition.model_dump_json(indent=2) + "\n")
self._lib_file(name).unlink()
self._commit(f"Detach and delete shared node '{name}'")
def usages(self, lib_name: str, draft: bool = True) -> list[str]:
"""Every ``flow.node`` pointing at this shared source.