feat(api): Add app_id field to HumanInputForm model

This ensures that `HumanInputForm` could be associated to a specific
application without relying on `WorkflowRun`, providing us a smoother
migration path if we want to implement test form.
This commit is contained in:
QuantumGhost
2026-01-14 16:58:17 +08:00
parent 25cc2ab738
commit f1b2e1cfb4
15 changed files with 73 additions and 29 deletions

View File

@ -25,7 +25,7 @@ from models import App
from models.enums import CreatorUserRole
from models.human_input import RecipientType
from models.model import AppMode
from models.workflow import Workflow, WorkflowRun
from models.workflow import WorkflowRun
from repositories.factory import DifyAPIRepositoryFactory
from services.human_input_service import Form, HumanInputService
@ -42,21 +42,10 @@ class ConsoleHumanInputFormApi(Resource):
@staticmethod
def _ensure_console_access(form: Form):
current_user, current_tenant_id = current_account_with_tenant()
_, current_tenant_id = current_account_with_tenant()
workflow_run = db.session.get(WorkflowRun, form.workflow_run_id)
if workflow_run is None or workflow_run.tenant_id != current_tenant_id:
raise NotFoundError("Workflow run not found")
if workflow_run.app_id:
app = db.session.get(App, workflow_run.app_id)
if app is None or app.tenant_id != current_tenant_id:
raise NotFoundError("App not found")
owner_account_id = app.created_by
else:
workflow = db.session.get(Workflow, workflow_run.workflow_id)
if workflow is None or workflow.tenant_id != current_tenant_id:
raise NotFoundError("Workflow not found")
if form.tenant_id != current_tenant_id:
raise NotFoundError("App not found")
@setup_required
@login_required

View File

@ -14,9 +14,8 @@ from controllers.web.error import NotFoundError
from controllers.web.site import serialize_site
from extensions.ext_database import db
from models.account import TenantStatus
from models.model import App, Site
from models.workflow import WorkflowRun
from models.human_input import RecipientType
from models.model import App, Site
from services.human_input_service import Form, FormNotFoundError, HumanInputService
logger = logging.getLogger(__name__)
@ -96,15 +95,9 @@ class HumanInputFormApi(Resource):
def _get_site_from_form(form: Form) -> Site:
"""Resolve Site for the form's workflow run and validate tenant status."""
workflow_run = (
db.session.query(WorkflowRun).where(WorkflowRun.id == form.workflow_run_id).first()
)
if workflow_run is None:
raise NotFoundError("Form not found")
app_model = db.session.query(App).where(App.id == workflow_run.app_id).first()
if app_model is None:
"""Resolve Site for the form's app and validate tenant status."""
app_model = db.session.query(App).where(App.id == form.app_id).first()
if app_model is None or app_model.tenant_id != form.tenant_id:
raise NotFoundError("Form not found")
site = db.session.query(Site).where(Site.app_id == app_model.id).first()

View File

@ -134,6 +134,7 @@ class HumanInputFormRecord:
workflow_run_id: str
node_id: str
tenant_id: str
app_id: str
definition: FormDefinition
rendered_content: str
expiration_time: datetime
@ -161,6 +162,7 @@ class HumanInputFormRecord:
workflow_run_id=form_model.workflow_run_id,
node_id=form_model.node_id,
tenant_id=form_model.tenant_id,
app_id=form_model.app_id,
definition=FormDefinition.model_validate_json(form_model.form_definition),
rendered_content=form_model.rendered_content,
expiration_time=form_model.expiration_time,
@ -334,6 +336,7 @@ class HumanInputFormRepositoryImpl:
form_model = HumanInputForm(
id=form_id,
tenant_id=self._tenant_id,
app_id=params.app_id,
workflow_run_id=params.workflow_execution_id,
node_id=params.node_id,
form_definition=form_definition.model_dump_json(),

View File

@ -208,6 +208,7 @@ class HumanInputNode(Node[HumanInputNodeData]):
if form is None:
display_in_ui = self._display_in_ui()
params = FormCreateParams(
app_id=self.app_id,
workflow_execution_id=self._workflow_execution_id,
node_id=self.id,
form_config=self._node_data,

View File

@ -18,6 +18,7 @@ class FormNotFoundError(HumanInputError):
@dataclasses.dataclass
class FormCreateParams:
app_id: str
workflow_execution_id: str
# node_id is the identifier for a specific

View File

@ -0,0 +1,29 @@
"""Add app_id to human_input_forms
Revision ID: 7a1c4d2f9b8e
Revises: e63797cc11c2
Create Date: 2026-01-14 00:01:00.000000
"""
from alembic import op
import models as models
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "7a1c4d2f9b8e"
down_revision = "e63797cc11c2"
branch_labels = None
depends_on = None
def upgrade():
op.add_column(
"human_input_forms",
sa.Column("app_id", models.types.StringUUID(), nullable=False),
)
def downgrade():
op.drop_column("human_input_forms", "app_id")

View File

@ -31,6 +31,7 @@ class HumanInputForm(DefaultFieldsMixin, Base):
__tablename__ = "human_input_forms"
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
workflow_run_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
# The human input node the current form corresponds to.

View File

@ -42,6 +42,14 @@ class Form:
def workflow_run_id(self) -> str:
return self._record.workflow_run_id
@property
def tenant_id(self) -> str:
return self._record.tenant_id
@property
def app_id(self) -> str:
return self._record.app_id
@property
def recipient_id(self) -> str | None:
return self._record.recipient_id

View File

@ -67,6 +67,7 @@ def _build_form_params(delivery_methods: list[EmailDeliveryMethod]) -> FormCreat
user_actions=[UserAction(id="approve", title="Approve")],
)
return FormCreateParams(
app_id="app-1",
workflow_execution_id=str(uuid4()),
node_id="human-input-node",
form_config=form_config,
@ -174,6 +175,7 @@ class TestHumanInputFormRepositoryImplWithContainers:
repository = HumanInputFormRepositoryImpl(session_factory=engine, tenant_id=tenant.id)
resolved_values = {"greeting": "Hello!"}
params = FormCreateParams(
app_id="app-1",
workflow_execution_id=str(uuid4()),
node_id="human-input-node",
form_config=HumanInputNodeData(
@ -209,6 +211,7 @@ class TestHumanInputFormRepositoryImplWithContainers:
repository = HumanInputFormRepositoryImpl(session_factory=engine, tenant_id=tenant.id)
params = FormCreateParams(
app_id="app-1",
workflow_execution_id=str(uuid4()),
node_id="human-input-node",
form_config=HumanInputNodeData(

View File

@ -123,6 +123,7 @@ def create_human_input_message_fixture(db_session) -> HumanInputMessageFixture:
)
form = HumanInputForm(
tenant_id=tenant.id,
app_id=app.id,
workflow_run_id=workflow_run_id,
node_id="node-id",
form_definition=form_definition.model_dump_json(),

View File

@ -88,6 +88,7 @@ def _build_form(db_session_with_containers, tenant, account):
engine = db_session_with_containers.get_bind()
repo = HumanInputFormRepositoryImpl(session_factory=engine, tenant_id=tenant.id)
params = FormCreateParams(
app_id="app-1",
workflow_execution_id=str(uuid.uuid4()),
node_id="node-1",
form_config=node_data,

View File

@ -38,7 +38,7 @@ class _FakeSession:
self._model_name = model.__name__
return self
def where(self, *args, **kwargs): # noqa: ANN002, ANN003
def where(self, *args, **kwargs):
return self
def first(self):
@ -63,6 +63,8 @@ def test_get_form_includes_site(monkeypatch: pytest.MonkeyPatch, app: Flask):
class _FakeForm:
workflow_run_id = "workflow-1"
app_id = "app-1"
tenant_id = "tenant-1"
def get_definition(self):
return _FakeDefinition()
@ -70,7 +72,7 @@ def test_get_form_includes_site(monkeypatch: pytest.MonkeyPatch, app: Flask):
form = _FakeForm()
tenant = SimpleNamespace(status=TenantStatus.NORMAL)
app_model = SimpleNamespace(id="app-1", tenant=tenant)
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", tenant=tenant)
workflow_run = SimpleNamespace(app_id="app-1")
site_model = SimpleNamespace(
title="My Site",
@ -122,13 +124,15 @@ def test_get_form_raises_forbidden_when_site_missing(monkeypatch: pytest.MonkeyP
class _FakeForm:
workflow_run_id = "workflow-1"
app_id = "app-1"
tenant_id = "tenant-1"
def get_definition(self):
return _FakeDefinition()
form = _FakeForm()
tenant = SimpleNamespace(status=TenantStatus.NORMAL)
app_model = SimpleNamespace(id="app-1", tenant=tenant)
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", tenant=tenant)
workflow_run = SimpleNamespace(app_id="app-1")
service_mock = MagicMock()

View File

@ -173,6 +173,7 @@ class _DummyForm:
workflow_run_id: str
node_id: str
tenant_id: str
app_id: str
form_definition: str
rendered_content: str
expiration_time: datetime
@ -283,6 +284,7 @@ class TestHumanInputFormRepositoryImplPublicMethods:
workflow_run_id="run-1",
node_id="node-1",
tenant_id="tenant-id",
app_id="app-id",
form_definition=_make_form_definition(),
rendered_content="<p>hello</p>",
expiration_time=naive_utc_now(),
@ -316,6 +318,7 @@ class TestHumanInputFormRepositoryImplPublicMethods:
workflow_run_id="run-1",
node_id="node-1",
tenant_id="tenant-id",
app_id="app-id",
form_definition=_make_form_definition(),
rendered_content="<p>hello</p>",
expiration_time=naive_utc_now(),
@ -336,6 +339,7 @@ class TestHumanInputFormRepositoryImplPublicMethods:
workflow_run_id="run-1",
node_id="node-1",
tenant_id="tenant-id",
app_id="app-id",
form_definition=_make_form_definition(),
rendered_content="<p>hello</p>",
expiration_time=naive_utc_now(),
@ -361,6 +365,7 @@ class TestHumanInputFormSubmissionRepository:
workflow_run_id="run-1",
node_id="node-1",
tenant_id="tenant-1",
app_id="app-1",
form_definition=_make_form_definition(),
rendered_content="<p>hello</p>",
expiration_time=naive_utc_now(),
@ -388,6 +393,7 @@ class TestHumanInputFormSubmissionRepository:
workflow_run_id="run-1",
node_id="node-1",
tenant_id="tenant-1",
app_id="app-1",
form_definition=_make_form_definition(),
rendered_content="<p>hello</p>",
expiration_time=naive_utc_now(),
@ -420,6 +426,7 @@ class TestHumanInputFormSubmissionRepository:
workflow_run_id="run-1",
node_id="node-1",
tenant_id="tenant-1",
app_id="app-1",
form_definition=_make_form_definition(),
rendered_content="<p>hello</p>",
expiration_time=fixed_now,

View File

@ -65,6 +65,7 @@ def _build_form(action_id: str, action_title: str, rendered_content: str) -> Hum
form = HumanInputForm(
id=f"form-{action_id}",
tenant_id="tenant-id",
app_id="app-id",
workflow_run_id="workflow-run",
node_id="node-id",
form_definition=definition.model_dump_json(),
@ -133,6 +134,7 @@ def test_get_by_message_ids_returns_unsubmitted_form_definition() -> None:
form = HumanInputForm(
id="form-1",
tenant_id="tenant-id",
app_id="app-id",
workflow_run_id="workflow-run",
node_id="node-id",
form_definition=definition.model_dump_json(),

View File

@ -41,6 +41,7 @@ def sample_form_record():
workflow_run_id="workflow-run-id",
node_id="node-id",
tenant_id="tenant-id",
app_id="app-id",
definition=FormDefinition(
form_content="hello",
inputs=[],