ref: architectural renames for deployment api, service, persistence layer (#12106)

Decouples deployment API schemas from
service-layer ID domains, updates naming to match persistence layer,
removes unnecessary database models, and adds strict API wrappers.
This commit is contained in:
Jordan Frazier
2026-03-09 11:14:39 -04:00
committed by GitHub
parent efb041ffd7
commit 8aa94539c1
9 changed files with 32 additions and 235 deletions

View File

@ -17,10 +17,11 @@ from langflow.api.v1.schemas.deployments import (
DeploymentDuplicateResponse,
DeploymentGetResponse,
DeploymentListResponse,
DeploymentProviderAccountCreate,
DeploymentProviderAccountCreateRequest,
DeploymentProviderAccountGetResponse,
DeploymentProviderAccountListResponse,
DeploymentProviderAccountResponse,
DeploymentProviderAccountUpdate,
DeploymentProviderAccountUpdateRequest,
DeploymentRedeployResponse,
DeploymentStatusResponse,
DeploymentTypeListResponse,
DeploymentUpdateRequest,
@ -29,7 +30,6 @@ from langflow.api.v1.schemas.deployments import (
ExecutionCreateResponse,
ExecutionStatusResponse,
FlowVersionIdsQuery,
RedeployResponse,
)
router = APIRouter(prefix="/deployments", tags=["Deployments"])
@ -63,13 +63,13 @@ DeploymentIdPath = Annotated[
@router.post(
"/providers",
response_model=DeploymentProviderAccountResponse,
response_model=DeploymentProviderAccountGetResponse,
status_code=status.HTTP_201_CREATED,
tags=["Deployment Providers"],
)
async def create_provider_account(
session: DbSession,
payload: DeploymentProviderAccountCreate,
payload: DeploymentProviderAccountCreateRequest,
current_user: CurrentActiveUser,
):
"""Register a new deployment provider account."""
@ -89,7 +89,7 @@ async def list_provider_accounts(
@router.get(
"/providers/{provider_id}",
response_model=DeploymentProviderAccountResponse,
response_model=DeploymentProviderAccountGetResponse,
tags=["Deployment Providers"],
)
async def get_provider_account(
@ -117,13 +117,13 @@ async def delete_provider_account(
@router.patch(
"/providers/{provider_id}",
response_model=DeploymentProviderAccountResponse,
response_model=DeploymentProviderAccountGetResponse,
tags=["Deployment Providers"],
)
async def update_provider_account(
provider_id: DeploymentProviderAccountIdPath,
session: DbSession,
payload: DeploymentProviderAccountUpdate,
payload: DeploymentProviderAccountUpdateRequest,
current_user: CurrentActiveUser,
):
"""Partially update a deployment provider account."""
@ -246,7 +246,7 @@ async def delete_deployment(
@router.post(
"/{deployment_id}/redeploy",
response_model=RedeployResponse,
response_model=DeploymentRedeployResponse,
)
async def redeploy_deployment(
deployment_id: DeploymentIdPath,

View File

@ -123,7 +123,7 @@ FlowVersionIdsQuery = Annotated[list[str] | None, AfterValidator(_validate_flow_
# ---------------------------------------------------------------------------
class DeploymentProviderAccountCreate(BaseModel):
class DeploymentProviderAccountCreateRequest(BaseModel):
model_config = {"extra": "forbid"}
provider_tenant_id: NonEmptyStr | None = Field(
@ -147,7 +147,7 @@ class DeploymentProviderAccountCreate(BaseModel):
return _normalize_str(value, field_name=info.field_name)
class DeploymentProviderAccountUpdate(BaseModel):
class DeploymentProviderAccountUpdateRequest(BaseModel):
model_config = {"extra": "forbid"}
provider_tenant_id: NonEmptyStr | None = Field(
@ -176,7 +176,7 @@ class DeploymentProviderAccountUpdate(BaseModel):
return _normalize_optional_str(value, field_name=info.field_name)
@model_validator(mode="after")
def ensure_any_field_provided(self) -> DeploymentProviderAccountUpdate:
def ensure_any_field_provided(self) -> DeploymentProviderAccountUpdateRequest:
if not self.model_fields_set:
msg = "At least one field must be provided for update."
raise ValueError(msg)
@ -189,7 +189,7 @@ class DeploymentProviderAccountUpdate(BaseModel):
return self
class DeploymentProviderAccountResponse(BaseModel):
class DeploymentProviderAccountGetResponse(BaseModel):
id: UUID = Field(description="Langflow DB provider-account UUID (`deployment_provider_account.id`).")
provider_tenant_id: str | None = Field(
default=None,
@ -264,7 +264,7 @@ class DeploymentListResponse(_PaginatedResponse):
class DeploymentProviderAccountListResponse(_PaginatedResponse):
providers: list[DeploymentProviderAccountResponse]
providers: list[DeploymentProviderAccountGetResponse]
class DeploymentCreateResponse(_DeploymentResponseBase):
@ -279,7 +279,7 @@ class DeploymentStatusResponse(_DeploymentResponseBase):
"""API response for deployment status/health."""
class RedeployResponse(_DeploymentResponseBase):
class DeploymentRedeployResponse(_DeploymentResponseBase):
"""API response for redeployment."""

View File

@ -1,3 +1,3 @@
from .model import Deployment, DeploymentCreate, DeploymentRead, DeploymentUpdate
from .model import Deployment, DeploymentRead
__all__ = ["Deployment", "DeploymentCreate", "DeploymentRead", "DeploymentUpdate"]
__all__ = ["Deployment", "DeploymentRead"]

View File

@ -8,7 +8,7 @@ from sqlalchemy import ForeignKey, UniqueConstraint
from sqlmodel import Column, DateTime, Field, Relationship, SQLModel, func
from langflow.schema.serialize import UUIDstr
from langflow.services.database.utils import validate_non_empty_string, validate_non_empty_string_optional
from langflow.services.database.utils import validate_non_empty_string
if TYPE_CHECKING:
from langflow.services.database.models.deployment_provider_account.model import DeploymentProviderAccount
@ -59,28 +59,6 @@ class Deployment(SQLModel, table=True): # type: ignore[call-arg]
return validate_non_empty_string(v, info)
class DeploymentCreate(SQLModel):
resource_key: str
deployment_provider_account_id: UUID
project_id: UUID
name: str
@field_validator("name", "resource_key")
@classmethod
def validate_non_empty(cls, v: str, info: object) -> str:
return validate_non_empty_string(v, info)
class DeploymentUpdate(SQLModel):
name: str | None = None
project_id: UUID | None = None
@field_validator("name", mode="before")
@classmethod
def validate_non_empty_if_provided(cls, v: str | None, info: object) -> str | None:
return validate_non_empty_string_optional(v, info)
class DeploymentRead(SQLModel):
id: UUID
resource_key: str

View File

@ -1,13 +1,9 @@
from .model import (
DeploymentProviderAccount,
DeploymentProviderAccountCreate,
DeploymentProviderAccountRead,
DeploymentProviderAccountUpdate,
)
__all__ = [
"DeploymentProviderAccount",
"DeploymentProviderAccountCreate",
"DeploymentProviderAccountRead",
"DeploymentProviderAccountUpdate",
]

View File

@ -11,7 +11,6 @@ from langflow.schema.serialize import UUIDstr
from langflow.services.database.utils import (
normalize_string_or_none,
validate_non_empty_string,
validate_non_empty_string_optional,
)
if TYPE_CHECKING:
@ -43,7 +42,7 @@ class DeploymentProviderAccount(SQLModel, table=True): # type: ignore[call-arg]
provider_key: str = Field(index=True)
provider_url: str = Field()
# MUST be stored encrypted; the CRUD layer encrypts via auth_utils before writing
# and the Read schema MUST intentionally excludes this field.
# and the Read schema intentionally excludes this field.
api_key: str = Field()
created_at: datetime | None = Field(
default=None,
@ -71,23 +70,6 @@ class DeploymentProviderAccount(SQLModel, table=True): # type: ignore[call-arg]
return validate_non_empty_string(v, info)
class DeploymentProviderAccountCreate(SQLModel):
provider_tenant_id: str | None = None
provider_key: str
provider_url: str
api_key: str
@field_validator("provider_tenant_id", mode="before")
@classmethod
def normalize_tenant_id(cls, v: str | None) -> str | None:
return normalize_string_or_none(v)
@field_validator("provider_key", "provider_url", "api_key")
@classmethod
def validate_non_empty(cls, v: str, info: object) -> str:
return validate_non_empty_string(v, info)
class DeploymentProviderAccountRead(SQLModel):
id: UUID
user_id: UUID
@ -97,25 +79,3 @@ class DeploymentProviderAccountRead(SQLModel):
created_at: datetime
updated_at: datetime
# api_key intentionally omitted -- stored encrypted, never serialize credentials to API responses
class DeploymentProviderAccountUpdate(SQLModel):
# All fields default to None. API routes consuming this schema must check
# ``model_fields_set`` to distinguish "field omitted" (keep existing value)
# from "field explicitly set to null" (clear the value). The CRUD layer's
# ``update_provider_account`` uses an ``_UNSET`` sentinel for the same
# purpose on ``provider_tenant_id``.
provider_tenant_id: str | None = None
provider_key: str | None = None
provider_url: str | None = None
api_key: str | None = None
@field_validator("provider_tenant_id", mode="before")
@classmethod
def normalize_tenant_id(cls, v: str | None) -> str | None:
return normalize_string_or_none(v)
@field_validator("provider_key", "provider_url", "api_key", mode="before")
@classmethod
def validate_non_empty_if_provided(cls, v: str | None, info: object) -> str | None:
return validate_non_empty_string_optional(v, info)

View File

@ -1,7 +1,7 @@
from unittest.mock import MagicMock
import pytest
from langflow.services.database.models.deployment.model import Deployment, DeploymentCreate, DeploymentRead
from langflow.services.database.models.deployment.model import Deployment, DeploymentRead
class TestDeploymentValidation:
@ -52,50 +52,3 @@ class TestDeploymentRead:
"updated_at",
}
assert set(DeploymentRead.model_fields.keys()) == expected
class TestDeploymentCreate:
"""Tests for DeploymentCreate schema."""
def test_rejects_empty_name(self):
from uuid import uuid4
with pytest.raises(ValueError, match="name must not be empty"):
DeploymentCreate(
resource_key="rk-1",
deployment_provider_account_id=uuid4(),
project_id=uuid4(),
name="",
)
def test_rejects_empty_resource_key(self):
from uuid import uuid4
with pytest.raises(ValueError, match="resource_key must not be empty"):
DeploymentCreate(
resource_key=" ",
deployment_provider_account_id=uuid4(),
project_id=uuid4(),
name="my-deploy",
)
def test_valid_create(self):
from uuid import uuid4
obj = DeploymentCreate(
resource_key="rk-1",
deployment_provider_account_id=uuid4(),
project_id=uuid4(),
name="my-deploy",
)
assert obj.name == "my-deploy"
assert obj.resource_key == "rk-1"
def test_has_expected_fields(self):
expected = {
"resource_key",
"deployment_provider_account_id",
"project_id",
"name",
}
assert set(DeploymentCreate.model_fields.keys()) == expected

View File

@ -3,9 +3,7 @@ from unittest.mock import MagicMock
import pytest
from langflow.services.database.models.deployment_provider_account.model import (
DeploymentProviderAccount,
DeploymentProviderAccountCreate,
DeploymentProviderAccountRead,
DeploymentProviderAccountUpdate,
)
@ -79,91 +77,3 @@ class TestDeploymentProviderAccountRead:
"updated_at",
}
assert set(DeploymentProviderAccountRead.model_fields.keys()) == expected
class TestDeploymentProviderAccountCreate:
"""Tests for DeploymentProviderAccountCreate schema validators."""
def test_rejects_empty_provider_key(self):
with pytest.raises(ValueError, match="provider_key must not be empty"):
DeploymentProviderAccountCreate(
provider_key="",
provider_url="https://example.com",
api_key="key", # pragma: allowlist secret
)
def test_rejects_empty_provider_url(self):
with pytest.raises(ValueError, match="provider_url must not be empty"):
DeploymentProviderAccountCreate(
provider_key="watsonx",
provider_url="",
api_key="key", # pragma: allowlist secret
)
def test_rejects_empty_api_key(self):
with pytest.raises(ValueError, match="api_key must not be empty"):
DeploymentProviderAccountCreate(provider_key="watsonx", provider_url="https://example.com", api_key="")
def test_valid_create(self):
obj = DeploymentProviderAccountCreate(
provider_key="watsonx",
provider_url="https://example.com",
api_key="key", # pragma: allowlist secret
)
assert obj.provider_key == "watsonx"
def test_blank_tenant_id_normalizes_to_none(self):
obj = DeploymentProviderAccountCreate(
provider_tenant_id=" ",
provider_key="watsonx",
provider_url="https://example.com",
api_key="key", # pragma: allowlist secret
)
assert obj.provider_tenant_id is None
def test_strips_tenant_id(self):
obj = DeploymentProviderAccountCreate(
provider_tenant_id=" tenant-1 ",
provider_key="watsonx",
provider_url="https://example.com",
api_key="key", # pragma: allowlist secret
)
assert obj.provider_tenant_id == "tenant-1"
class TestDeploymentProviderAccountUpdate:
"""Tests for DeploymentProviderAccountUpdate schema validators."""
def test_allows_none_values(self):
obj = DeploymentProviderAccountUpdate()
assert obj.provider_key is None
assert obj.provider_url is None
assert obj.api_key is None
def test_rejects_empty_provider_key_when_provided(self):
with pytest.raises(ValueError, match="provider_key must not be empty"):
DeploymentProviderAccountUpdate(provider_key="")
def test_rejects_empty_provider_url_when_provided(self):
with pytest.raises(ValueError, match="provider_url must not be empty"):
DeploymentProviderAccountUpdate(provider_url="")
def test_rejects_empty_api_key_when_provided(self):
with pytest.raises(ValueError, match="api_key must not be empty"):
DeploymentProviderAccountUpdate(api_key="")
def test_rejects_whitespace_provider_key_when_provided(self):
with pytest.raises(ValueError, match="provider_key must not be empty"):
DeploymentProviderAccountUpdate(provider_key=" ")
def test_valid_update(self):
obj = DeploymentProviderAccountUpdate(provider_key="new-key")
assert obj.provider_key == "new-key"
def test_blank_tenant_id_normalizes_to_none(self):
obj = DeploymentProviderAccountUpdate(provider_tenant_id=" ")
assert obj.provider_tenant_id is None
def test_strips_tenant_id(self):
obj = DeploymentProviderAccountUpdate(provider_tenant_id=" tenant-1 ")
assert obj.provider_tenant_id == "tenant-1"

View File

@ -7,9 +7,9 @@ from uuid import uuid4
import pytest
from langflow.api.v1.schemas.deployments import (
DeploymentProviderAccountCreate,
DeploymentProviderAccountResponse,
DeploymentProviderAccountUpdate,
DeploymentProviderAccountCreateRequest,
DeploymentProviderAccountGetResponse,
DeploymentProviderAccountUpdateRequest,
FlowVersionsAttach,
FlowVersionsPatch,
)
@ -24,12 +24,12 @@ class TestApiKeyWriteOnly:
"""Ensure api_key is excluded from every response model."""
def test_provider_account_response_excludes_api_key(self):
"""DeploymentProviderAccountResponse.model_fields must not contain api_key."""
assert "api_key" not in DeploymentProviderAccountResponse.model_fields
"""DeploymentProviderAccountGetResponse.model_fields must not contain api_key."""
assert "api_key" not in DeploymentProviderAccountGetResponse.model_fields
def test_provider_account_response_dump_excludes_api_key(self):
"""model_dump() on a response instance must never contain api_key."""
response = DeploymentProviderAccountResponse(
response = DeploymentProviderAccountGetResponse(
id=uuid4(),
provider_key="aws",
provider_url="https://example.com",
@ -39,7 +39,7 @@ class TestApiKeyWriteOnly:
def test_create_schema_masks_api_key_in_repr(self):
"""SecretStr should mask the value in string representations."""
account = DeploymentProviderAccountCreate(
account = DeploymentProviderAccountCreateRequest(
provider_key="aws",
provider_url="https://example.com",
api_key="super-secret-key",
@ -49,7 +49,7 @@ class TestApiKeyWriteOnly:
def test_update_schema_masks_api_key_in_repr(self):
"""SecretStr should mask the value in string representations on update."""
account = DeploymentProviderAccountUpdate(api_key="new-secret")
account = DeploymentProviderAccountUpdateRequest(api_key="new-secret")
assert isinstance(account.api_key, SecretStr)
assert "new-secret" not in repr(account)
@ -61,7 +61,7 @@ class TestApiKeyWriteOnly:
class TestNonEmptyStr:
def test_strips_whitespace(self):
account = DeploymentProviderAccountCreate(
account = DeploymentProviderAccountCreateRequest(
provider_key=" aws ",
provider_url="https://example.com",
api_key="key",
@ -70,7 +70,7 @@ class TestNonEmptyStr:
def test_rejects_empty_string(self):
with pytest.raises(ValidationError):
DeploymentProviderAccountCreate(
DeploymentProviderAccountCreateRequest(
provider_key="",
provider_url="https://example.com",
api_key="key",
@ -78,7 +78,7 @@ class TestNonEmptyStr:
def test_rejects_whitespace_only(self):
with pytest.raises(ValidationError):
DeploymentProviderAccountCreate(
DeploymentProviderAccountCreateRequest(
provider_key=" ",
provider_url="https://example.com",
api_key="key",