mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 21:28:25 +08:00
feat(inner_api): batch-metadata endpoint for EE permitted-apps flow
This commit is contained in:
parent
35d9b6a0f8
commit
7b6ceaebea
@ -17,6 +17,7 @@ inner_api_ns = Namespace("inner_api", description="Internal API operations", pat
|
||||
|
||||
from . import mail as _mail
|
||||
from .app import dsl as _app_dsl
|
||||
from .app import metadata as _app_metadata
|
||||
from .plugin import plugin as _plugin
|
||||
from .workspace import workspace as _workspace
|
||||
|
||||
@ -24,6 +25,7 @@ api.add_namespace(inner_api_ns)
|
||||
|
||||
__all__ = [
|
||||
"_app_dsl",
|
||||
"_app_metadata",
|
||||
"_mail",
|
||||
"_plugin",
|
||||
"_workspace",
|
||||
|
||||
66
api/controllers/inner_api/app/metadata.py
Normal file
66
api/controllers/inner_api/app/metadata.py
Normal file
@ -0,0 +1,66 @@
|
||||
"""Inner endpoint: batch-fetch app metadata for the EE permitted-apps flow.
|
||||
|
||||
Called by ``dify-enterprise`` server to hydrate ``web_app_settings.app_id`` rows
|
||||
with their ``apps`` table fields. Filters out non-``normal`` apps server-side.
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
||||
|
||||
from controllers.common.schema import register_schema_model
|
||||
from controllers.console.wraps import setup_required
|
||||
from controllers.inner_api import inner_api_ns
|
||||
from controllers.inner_api.wraps import enterprise_inner_api_only
|
||||
from extensions.ext_database import db
|
||||
from models import App
|
||||
from models.enums import AppStatus
|
||||
|
||||
|
||||
class InnerAppBatchMetadataPayload(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
ids: list[str] = Field(min_length=1, max_length=500, description="App UUIDs to fetch metadata for (1-500 entries)")
|
||||
|
||||
|
||||
register_schema_model(inner_api_ns, InnerAppBatchMetadataPayload)
|
||||
|
||||
|
||||
@inner_api_ns.route("/enterprise/apps/batch-metadata")
|
||||
class InnerAppBatchMetadataApi(Resource):
|
||||
@setup_required
|
||||
@enterprise_inner_api_only
|
||||
@inner_api_ns.doc("enterprise_app_batch_metadata")
|
||||
@inner_api_ns.expect(inner_api_ns.models[InnerAppBatchMetadataPayload.__name__])
|
||||
@inner_api_ns.doc(
|
||||
responses={
|
||||
200: "Batch metadata returned",
|
||||
401: "Inner API key invalid",
|
||||
422: "Payload validation failed",
|
||||
}
|
||||
)
|
||||
def post(self):
|
||||
"""Batch-fetch app metadata for the EE permitted-apps flow."""
|
||||
try:
|
||||
payload = InnerAppBatchMetadataPayload.model_validate(inner_api_ns.payload or {})
|
||||
except ValidationError as exc:
|
||||
return {"message": exc.json()}, 422
|
||||
|
||||
rows = (
|
||||
db.session.execute(sa.select(App).where(App.id.in_(payload.ids), App.status == AppStatus.NORMAL))
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
return {
|
||||
"data": [
|
||||
{
|
||||
"id": str(app.id),
|
||||
"tenant_id": str(app.tenant_id),
|
||||
"mode": app.mode,
|
||||
"name": app.name,
|
||||
"updated_at": app.updated_at.isoformat(),
|
||||
}
|
||||
for app in rows
|
||||
]
|
||||
}, 200
|
||||
138
api/tests/unit_tests/controllers/inner_api/test_app_metadata.py
Normal file
138
api/tests/unit_tests/controllers/inner_api/test_app_metadata.py
Normal file
@ -0,0 +1,138 @@
|
||||
"""Inner endpoint to batch-fetch app metadata for the EE permitted-apps flow."""
|
||||
|
||||
import builtins
|
||||
import inspect
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
from flask.views import MethodView
|
||||
from pydantic import ValidationError
|
||||
|
||||
from controllers.inner_api import bp as inner_api_bp
|
||||
from controllers.inner_api.app.metadata import InnerAppBatchMetadataApi, InnerAppBatchMetadataPayload
|
||||
|
||||
if not hasattr(builtins, "MethodView"):
|
||||
builtins.MethodView = MethodView # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def inner_app() -> Flask:
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.register_blueprint(inner_api_bp)
|
||||
return app
|
||||
|
||||
|
||||
def test_route_registered(inner_app: Flask):
|
||||
rules = {r.rule for r in inner_app.url_map.iter_rules()}
|
||||
assert "/inner/api/enterprise/apps/batch-metadata" in rules
|
||||
|
||||
|
||||
def test_dispatches_to_class(inner_app: Flask):
|
||||
rule = next(r for r in inner_app.url_map.iter_rules() if r.rule == "/inner/api/enterprise/apps/batch-metadata")
|
||||
assert inner_app.view_functions[rule.endpoint].view_class is InnerAppBatchMetadataApi
|
||||
|
||||
|
||||
def test_post_method_only(inner_app: Flask):
|
||||
rule = next(r for r in inner_app.url_map.iter_rules() if r.rule == "/inner/api/enterprise/apps/batch-metadata")
|
||||
assert "POST" in rule.methods
|
||||
assert "GET" not in rule.methods
|
||||
|
||||
|
||||
def test_payload_rejects_empty_ids():
|
||||
with pytest.raises(ValidationError):
|
||||
InnerAppBatchMetadataPayload.model_validate({"ids": []})
|
||||
|
||||
|
||||
def test_payload_rejects_too_many_ids():
|
||||
with pytest.raises(ValidationError):
|
||||
InnerAppBatchMetadataPayload.model_validate({"ids": ["x"] * 501})
|
||||
|
||||
|
||||
def _make_app_row(*, id=None, tenant_id=None, mode="chat", name="Test", updated_at=None, status="normal"):
|
||||
"""Build a stand-in App row for handler tests."""
|
||||
row = MagicMock()
|
||||
row.id = id or uuid.uuid4()
|
||||
row.tenant_id = tenant_id or uuid.uuid4()
|
||||
row.mode = mode
|
||||
row.name = name
|
||||
row.status = status
|
||||
row.updated_at = updated_at or datetime(2026, 5, 6, 12, 0, 0, tzinfo=UTC)
|
||||
return row
|
||||
|
||||
|
||||
def test_post_returns_metadata_for_normal_apps(inner_app: Flask):
|
||||
app_row = _make_app_row(name="App A", mode="workflow")
|
||||
|
||||
api_instance = InnerAppBatchMetadataApi()
|
||||
handler = inspect.unwrap(api_instance.post)
|
||||
|
||||
with inner_app.test_request_context(
|
||||
path="/inner/api/enterprise/apps/batch-metadata",
|
||||
method="POST",
|
||||
json={"ids": [str(app_row.id)]},
|
||||
):
|
||||
with (
|
||||
patch("controllers.inner_api.app.metadata.inner_api_ns") as mock_ns,
|
||||
patch("controllers.inner_api.app.metadata.db") as mock_db,
|
||||
):
|
||||
mock_ns.payload = {"ids": [str(app_row.id)]}
|
||||
mock_scalars = MagicMock()
|
||||
mock_scalars.all.return_value = [app_row]
|
||||
mock_db.session.execute.return_value.scalars.return_value = mock_scalars
|
||||
|
||||
body, status = handler(api_instance)
|
||||
|
||||
assert status == 200
|
||||
assert body == {
|
||||
"data": [
|
||||
{
|
||||
"id": str(app_row.id),
|
||||
"tenant_id": str(app_row.tenant_id),
|
||||
"mode": "workflow",
|
||||
"name": "App A",
|
||||
"updated_at": "2026-05-06T12:00:00+00:00",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_post_returns_empty_data_when_no_apps(inner_app: Flask):
|
||||
api_instance = InnerAppBatchMetadataApi()
|
||||
handler = inspect.unwrap(api_instance.post)
|
||||
|
||||
with inner_app.test_request_context(
|
||||
path="/inner/api/enterprise/apps/batch-metadata",
|
||||
method="POST",
|
||||
json={"ids": [str(uuid.uuid4())]},
|
||||
):
|
||||
with (
|
||||
patch("controllers.inner_api.app.metadata.inner_api_ns") as mock_ns,
|
||||
patch("controllers.inner_api.app.metadata.db") as mock_db,
|
||||
):
|
||||
mock_ns.payload = {"ids": [str(uuid.uuid4())]}
|
||||
mock_scalars = MagicMock()
|
||||
mock_scalars.all.return_value = []
|
||||
mock_db.session.execute.return_value.scalars.return_value = mock_scalars
|
||||
|
||||
body, status = handler(api_instance)
|
||||
|
||||
assert status == 200
|
||||
assert body == {"data": []}
|
||||
|
||||
|
||||
def test_post_returns_422_on_invalid_payload(inner_app: Flask):
|
||||
api_instance = InnerAppBatchMetadataApi()
|
||||
handler = inspect.unwrap(api_instance.post)
|
||||
|
||||
with inner_app.test_request_context(path="/inner/api/enterprise/apps/batch-metadata", method="POST"):
|
||||
with patch("controllers.inner_api.app.metadata.inner_api_ns") as mock_ns:
|
||||
mock_ns.payload = {"ids": []} # min_length=1 violation
|
||||
|
||||
body, status = handler(api_instance)
|
||||
|
||||
assert status == 422
|
||||
assert "message" in body
|
||||
Loading…
Reference in New Issue
Block a user