Merge branch 'main' into jzh

This commit is contained in:
JzoNg
2026-04-01 09:26:13 +08:00
78 changed files with 556 additions and 283 deletions

View File

@@ -2,7 +2,7 @@ import flask_restx
from flask_restx import Resource, fields, marshal_with
from flask_restx._http import HTTPStatus
from sqlalchemy import delete, func, select
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import Forbidden
from extensions.ext_database import db
@@ -34,7 +34,7 @@ api_key_list_model = console_ns.model(
def _get_resource(resource_id, tenant_id, resource_model):
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
resource = session.execute(
select(resource_model).filter_by(id=resource_id, tenant_id=tenant_id)
).scalar_one_or_none()

View File

@@ -9,7 +9,7 @@ from graphon.enums import WorkflowExecutionStatus
from graphon.file import helpers as file_helpers
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, computed_field, field_validator
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import BadRequest
from controllers.common.helpers import FileInfo
@@ -642,7 +642,7 @@ class AppCopyApi(Resource):
args = CopyAppPayload.model_validate(console_ns.payload or {})
with Session(db.engine) as session:
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
import_service = AppDslService(session)
yaml_content = import_service.export_dsl(app_model=app_model, include_secret=True)
result = import_service.import_app(
@@ -655,7 +655,6 @@ class AppCopyApi(Resource):
icon=args.icon,
icon_background=args.icon_background,
)
session.commit()
# Inherit web app permission from original app
if result.app_id and FeatureService.get_system_features().webapp_auth.enabled:

View File

@@ -1,6 +1,6 @@
from flask_restx import Resource, fields, marshal_with
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import (
@@ -71,7 +71,7 @@ class AppImportApi(Resource):
args = AppImportPayload.model_validate(console_ns.payload)
# Create service with session
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
import_service = AppDslService(session)
# Import app
account = current_user
@@ -87,7 +87,6 @@ class AppImportApi(Resource):
icon_background=args.icon_background,
app_id=args.app_id,
)
session.commit()
if result.app_id and FeatureService.get_system_features().webapp_auth.enabled:
# update web app setting as private
EnterpriseService.WebAppAuth.update_app_access_mode(result.app_id, "private")
@@ -112,12 +111,11 @@ class AppImportConfirmApi(Resource):
current_user, _ = current_account_with_tenant()
# Create service with session
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
import_service = AppDslService(session)
# Confirm import
account = current_user
result = import_service.confirm_import(import_id=import_id, account=account)
session.commit()
# Return appropriate status code based on result
if result.status == ImportStatus.FAILED:
@@ -134,7 +132,7 @@ class AppImportCheckDependenciesApi(Resource):
@marshal_with(app_import_check_dependencies_model)
@edit_permission_required
def get(self, app_model: App):
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
import_service = AppDslService(session)
result = import_service.check_dependencies(app_model=app_model)

View File

@@ -2,7 +2,7 @@ from flask import request
from flask_restx import Resource, fields, marshal_with
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from controllers.console import console_ns
from controllers.console.app.wraps import get_app_model
@@ -69,7 +69,7 @@ class ConversationVariablesApi(Resource):
page_size = 100
stmt = stmt.limit(page_size).offset((page - 1) * page_size)
with Session(db.engine) as session:
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
rows = session.scalars(stmt).all()
return {

View File

@@ -10,7 +10,7 @@ from graphon.file import File
from graphon.graph_engine.manager import GraphEngineManager
from graphon.model_runtime.utils.encoders import jsonable_encoder
from pydantic import BaseModel, Field, field_validator
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import BadRequest, Forbidden, InternalServerError, NotFound
import services
@@ -840,7 +840,7 @@ class PublishedWorkflowApi(Resource):
args = PublishWorkflowPayload.model_validate(console_ns.payload or {})
workflow_service = WorkflowService()
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
workflow = workflow_service.publish_workflow(
session=session,
app_model=app_model,
@@ -858,8 +858,6 @@ class PublishedWorkflowApi(Resource):
workflow_created_at = TimestampField().format(workflow.created_at)
session.commit()
return {
"result": "success",
"created_at": workflow_created_at,
@@ -982,7 +980,7 @@ class PublishedAllWorkflowApi(Resource):
raise Forbidden()
workflow_service = WorkflowService()
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
workflows, has_more = workflow_service.get_all_published_workflow(
session=session,
app_model=app_model,
@@ -1072,7 +1070,7 @@ class WorkflowByIdApi(Resource):
workflow_service = WorkflowService()
# Create a session and manage the transaction
with Session(db.engine, expire_on_commit=False) as session:
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
workflow = workflow_service.update_workflow(
session=session,
workflow_id=workflow_id,
@@ -1084,9 +1082,6 @@ class WorkflowByIdApi(Resource):
if not workflow:
raise NotFound("Workflow not found")
# Commit the transaction in the controller
session.commit()
return workflow
@setup_required
@@ -1101,13 +1096,11 @@ class WorkflowByIdApi(Resource):
workflow_service = WorkflowService()
# Create a session and manage the transaction
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
try:
workflow_service.delete_workflow(
session=session, workflow_id=workflow_id, tenant_id=app_model.tenant_id
)
# Commit the transaction in the controller
session.commit()
except WorkflowInUseError as e:
abort(400, description=str(e))
except DraftWorkflowDeletionError as e:

View File

@@ -5,7 +5,7 @@ from flask import request
from flask_restx import Resource, marshal_with
from graphon.enums import WorkflowExecutionStatus
from pydantic import BaseModel, Field, field_validator
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from controllers.console import console_ns
from controllers.console.app.wraps import get_app_model
@@ -87,7 +87,7 @@ class WorkflowAppLogApi(Resource):
# get paginate workflow app logs
workflow_app_service = WorkflowAppService()
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
workflow_app_log_pagination = workflow_app_service.get_paginate_workflow_app_logs(
session=session,
app_model=app_model,
@@ -124,7 +124,7 @@ class WorkflowArchivedLogApi(Resource):
args = WorkflowAppLogQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore
workflow_app_service = WorkflowAppService()
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
workflow_app_log_pagination = workflow_app_service.get_paginate_workflow_archive_logs(
session=session,
app_model=app_model,

View File

@@ -10,7 +10,7 @@ from graphon.variables.segment_group import SegmentGroup
from graphon.variables.segments import ArrayFileSegment, FileSegment, Segment
from graphon.variables.types import SegmentType
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from controllers.console import console_ns
from controllers.console.app.error import (
@@ -244,7 +244,7 @@ class WorkflowVariableCollectionApi(Resource):
raise DraftWorkflowNotExist()
# fetch draft workflow by app_model
with Session(bind=db.engine, expire_on_commit=False) as session:
with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session:
draft_var_srv = WorkflowDraftVariableService(
session=session,
)
@@ -298,7 +298,7 @@ class NodeVariableCollectionApi(Resource):
@marshal_with(workflow_draft_variable_list_model)
def get(self, app_model: App, node_id: str):
validate_node_id(node_id)
with Session(bind=db.engine, expire_on_commit=False) as session:
with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session:
draft_var_srv = WorkflowDraftVariableService(
session=session,
)
@@ -465,7 +465,7 @@ class VariableResetApi(Resource):
def _get_variable_list(app_model: App, node_id) -> WorkflowDraftVariableList:
with Session(bind=db.engine, expire_on_commit=False) as session:
with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session:
draft_var_srv = WorkflowDraftVariableService(
session=session,
)

View File

@@ -4,7 +4,7 @@ from flask import request
from flask_restx import Resource, fields, marshal_with
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import NotFound
from configs import dify_config
@@ -64,7 +64,7 @@ class WebhookTriggerApi(Resource):
node_id = args.node_id
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
# Get webhook trigger for this app and node
webhook_trigger = (
session.query(WorkflowWebhookTrigger)
@@ -95,7 +95,7 @@ class AppTriggersApi(Resource):
assert isinstance(current_user, Account)
assert current_user.current_tenant_id is not None
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
# Get all triggers for this app using select API
triggers = (
session.execute(
@@ -137,7 +137,7 @@ class AppTriggerEnableApi(Resource):
assert current_user.current_tenant_id is not None
trigger_id = args.trigger_id
with Session(db.engine) as session:
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
# Find the trigger using select
trigger = session.execute(
select(AppTrigger).where(
@@ -153,9 +153,6 @@ class AppTriggerEnableApi(Resource):
# Update status based on enable_trigger boolean
trigger.status = AppTriggerStatus.ENABLED if args.enable_trigger else AppTriggerStatus.DISABLED
session.commit()
session.refresh(trigger)
# Add computed icon field
url_prefix = dify_config.CONSOLE_API_URL + "/console/api/workspaces/current/tool-provider/builtin/"
if trigger.trigger_type == "trigger-plugin":

View File

@@ -36,7 +36,7 @@ class Subscription(Resource):
@only_edition_cloud
def get(self):
current_user, current_tenant_id = current_account_with_tenant()
args = SubscriptionQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore
args = SubscriptionQuery.model_validate(request.args.to_dict(flat=True))
BillingService.is_tenant_owner_or_admin(current_user)
return BillingService.get_subscription(args.plan, args.interval, current_user.email, current_tenant_id)

View File

@@ -31,7 +31,7 @@ class ComplianceApi(Resource):
@only_edition_cloud
def get(self):
current_user, current_tenant_id = current_account_with_tenant()
args = ComplianceDownloadQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore
args = ComplianceDownloadQuery.model_validate(request.args.to_dict(flat=True))
ip_address = extract_remote_ip(request)
device_info = request.headers.get("User-Agent", "Unknown device")

View File

@@ -6,7 +6,7 @@ from flask import request
from flask_restx import Resource, fields, marshal_with
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import NotFound
from controllers.common.schema import get_or_create_model, register_schema_model
@@ -159,7 +159,7 @@ class DataSourceApi(Resource):
@account_initialization_required
def patch(self, binding_id, action: Literal["enable", "disable"]):
binding_id = str(binding_id)
with Session(db.engine) as session:
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
data_source_binding = session.execute(
select(DataSourceOauthBinding).filter_by(id=binding_id)
).scalar_one_or_none()
@@ -211,7 +211,7 @@ class DataSourceNotionListApi(Resource):
if not credential:
raise NotFound("Credential not found.")
exist_page_ids = []
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
# import notion in the exist dataset
if query.dataset_id:
dataset = DatasetService.get_dataset(query.dataset_id)

View File

@@ -3,7 +3,7 @@ import logging
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from controllers.common.schema import register_schema_models
from controllers.console import console_ns
@@ -85,7 +85,7 @@ class CustomizedPipelineTemplateApi(Resource):
@account_initialization_required
@enterprise_license_required
def post(self, template_id: str):
with Session(db.engine) as session:
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
template = (
session.query(PipelineCustomizedTemplate).where(PipelineCustomizedTemplate.id == template_id).first()
)

View File

@@ -1,6 +1,6 @@
from flask_restx import Resource, marshal
from pydantic import BaseModel
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import Forbidden
import services
@@ -54,7 +54,7 @@ class CreateRagPipelineDatasetApi(Resource):
yaml_content=payload.yaml_content,
)
try:
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
rag_pipeline_dsl_service = RagPipelineDslService(session)
import_info = rag_pipeline_dsl_service.create_rag_pipeline_dataset(
tenant_id=current_tenant_id,

View File

@@ -5,7 +5,7 @@ from flask import Response, request
from flask_restx import Resource, marshal, marshal_with
from graphon.variables.types import SegmentType
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import Forbidden
from controllers.common.schema import register_schema_models
@@ -96,7 +96,7 @@ class RagPipelineVariableCollectionApi(Resource):
raise DraftWorkflowNotExist()
# fetch draft workflow by app_model
with Session(bind=db.engine, expire_on_commit=False) as session:
with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session:
draft_var_srv = WorkflowDraftVariableService(
session=session,
)
@@ -143,7 +143,7 @@ class RagPipelineNodeVariableCollectionApi(Resource):
@marshal_with(workflow_draft_variable_list_model)
def get(self, pipeline: Pipeline, node_id: str):
validate_node_id(node_id)
with Session(bind=db.engine, expire_on_commit=False) as session:
with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session:
draft_var_srv = WorkflowDraftVariableService(
session=session,
)
@@ -289,7 +289,7 @@ class RagPipelineVariableResetApi(Resource):
def _get_variable_list(pipeline: Pipeline, node_id) -> WorkflowDraftVariableList:
with Session(bind=db.engine, expire_on_commit=False) as session:
with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session:
draft_var_srv = WorkflowDraftVariableService(
session=session,
)

View File

@@ -1,7 +1,7 @@
from flask import request
from flask_restx import Resource, fields, marshal_with # type: ignore
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from controllers.common.schema import get_or_create_model, register_schema_models
from controllers.console import console_ns
@@ -68,7 +68,7 @@ class RagPipelineImportApi(Resource):
payload = RagPipelineImportPayload.model_validate(console_ns.payload or {})
# Create service with session
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
import_service = RagPipelineDslService(session)
# Import app
account = current_user
@@ -80,7 +80,6 @@ class RagPipelineImportApi(Resource):
pipeline_id=payload.pipeline_id,
dataset_name=payload.name,
)
session.commit()
# Return appropriate status code based on result
status = result.status
@@ -102,12 +101,11 @@ class RagPipelineImportConfirmApi(Resource):
current_user, _ = current_account_with_tenant()
# Create service with session
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
import_service = RagPipelineDslService(session)
# Confirm import
account = current_user
result = import_service.confirm_import(import_id=import_id, account=account)
session.commit()
# Return appropriate status code based on result
if result.status == ImportStatus.FAILED:
@@ -124,7 +122,7 @@ class RagPipelineImportCheckDependenciesApi(Resource):
@edit_permission_required
@marshal_with(pipeline_import_check_dependencies_model)
def get(self, pipeline: Pipeline):
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
import_service = RagPipelineDslService(session)
result = import_service.check_dependencies(pipeline=pipeline)
@@ -142,7 +140,7 @@ class RagPipelineExportApi(Resource):
# Add include_secret params
query = IncludeSecretQuery.model_validate(request.args.to_dict())
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
export_service = RagPipelineDslService(session)
result = export_service.export_rag_pipeline_dsl(
pipeline=pipeline, include_secret=query.include_secret == "true"

View File

@@ -6,7 +6,7 @@ from flask import abort, request
from flask_restx import Resource, marshal_with # type: ignore
from graphon.model_runtime.utils.encoders import jsonable_encoder
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import BadRequest, Forbidden, InternalServerError, NotFound
import services
@@ -608,7 +608,7 @@ class PublishedRagPipelineApi(Resource):
# The role of the current user in the ta table must be admin, owner, or editor
current_user, _ = current_account_with_tenant()
rag_pipeline_service = RagPipelineService()
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
pipeline = session.merge(pipeline)
workflow = rag_pipeline_service.publish_workflow(
session=session,
@@ -620,8 +620,6 @@ class PublishedRagPipelineApi(Resource):
session.add(pipeline)
workflow_created_at = TimestampField().format(workflow.created_at)
session.commit()
return {
"result": "success",
"created_at": workflow_created_at,
@@ -695,7 +693,7 @@ class PublishedAllRagPipelineApi(Resource):
raise Forbidden()
rag_pipeline_service = RagPipelineService()
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
workflows, has_more = rag_pipeline_service.get_all_published_workflow(
session=session,
pipeline=pipeline,
@@ -767,7 +765,7 @@ class RagPipelineByIdApi(Resource):
rag_pipeline_service = RagPipelineService()
# Create a session and manage the transaction
with Session(db.engine, expire_on_commit=False) as session:
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
workflow = rag_pipeline_service.update_workflow(
session=session,
workflow_id=workflow_id,
@@ -779,9 +777,6 @@ class RagPipelineByIdApi(Resource):
if not workflow:
raise NotFound("Workflow not found")
# Commit the transaction in the controller
session.commit()
return workflow
@setup_required
@@ -798,14 +793,13 @@ class RagPipelineByIdApi(Resource):
workflow_service = WorkflowService()
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
try:
workflow_service.delete_workflow(
session=session,
workflow_id=workflow_id,
tenant_id=pipeline.tenant_id,
)
session.commit()
except WorkflowInUseError as e:
abort(400, description=str(e))
except DraftWorkflowDeletionError as e:

View File

@@ -2,7 +2,7 @@ from typing import Any
from flask import request
from pydantic import BaseModel, Field, TypeAdapter, model_validator
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import NotFound
from controllers.common.schema import register_schema_models
@@ -74,7 +74,7 @@ class ConversationListApi(InstalledAppResource):
try:
if not isinstance(current_user, Account):
raise ValueError("current_user must be an Account instance")
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
pagination = WebConversationService.pagination_by_last_id(
session=session,
app_model=app_model,

View File

@@ -2,7 +2,7 @@ from collections.abc import Callable
from functools import wraps
from typing import ParamSpec, TypeVar
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import Forbidden
from extensions.ext_database import db
@@ -24,7 +24,7 @@ def plugin_permission_required(
user = current_user
tenant_id = current_tenant_id
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
permission = (
session.query(TenantPluginPermission)
.where(

View File

@@ -8,7 +8,7 @@ from flask import request
from flask_restx import Resource, fields, marshal_with
from pydantic import BaseModel, Field, field_validator, model_validator
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from configs import dify_config
from constants.languages import supported_language
@@ -519,7 +519,7 @@ class EducationAutoCompleteApi(Resource):
@cloud_edition_billing_enabled
@marshal_with(data_fields)
def get(self):
payload = request.args.to_dict(flat=True) # type: ignore
payload = request.args.to_dict(flat=True)
args = EducationAutocompleteQuery.model_validate(payload)
return BillingService.EducationIdentity.autocomplete(args.keywords, args.page, args.limit)
@@ -562,7 +562,7 @@ class ChangeEmailSendEmailApi(Resource):
user_email = current_user.email
else:
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
account = AccountService.get_account_by_email_with_case_fallback(args.email, session=session)
if account is None:
raise AccountNotFound()

View File

@@ -99,7 +99,7 @@ class ModelProviderListApi(Resource):
_, current_tenant_id = current_account_with_tenant()
tenant_id = current_tenant_id
payload = request.args.to_dict(flat=True) # type: ignore
payload = request.args.to_dict(flat=True)
args = ParserModelList.model_validate(payload)
model_provider_service = ModelProviderService()
@@ -118,7 +118,7 @@ class ModelProviderCredentialApi(Resource):
_, current_tenant_id = current_account_with_tenant()
tenant_id = current_tenant_id
# if credential_id is not provided, return current used credential
payload = request.args.to_dict(flat=True) # type: ignore
payload = request.args.to_dict(flat=True)
args = ParserCredentialId.model_validate(payload)
model_provider_service = ModelProviderService()

View File

@@ -7,7 +7,7 @@ from flask import make_response, redirect, request, send_file
from flask_restx import Resource
from graphon.model_runtime.utils.encoders import jsonable_encoder
from pydantic import BaseModel, Field, HttpUrl, field_validator, model_validator
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import Forbidden
from configs import dify_config
@@ -1019,7 +1019,7 @@ class ToolProviderMCPApi(Resource):
# Step 1: Get provider data for URL validation (short-lived session, no network I/O)
validation_data = None
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
service = MCPToolManageService(session=session)
validation_data = service.get_provider_for_url_validation(
tenant_id=current_tenant_id, provider_id=payload.provider_id
@@ -1034,7 +1034,7 @@ class ToolProviderMCPApi(Resource):
)
# Step 3: Perform database update in a transaction
with Session(db.engine) as session, session.begin():
with sessionmaker(db.engine).begin() as session:
service = MCPToolManageService(session=session)
service.update_provider(
tenant_id=current_tenant_id,
@@ -1061,7 +1061,7 @@ class ToolProviderMCPApi(Resource):
payload = MCPProviderDeletePayload.model_validate(console_ns.payload or {})
_, current_tenant_id = current_account_with_tenant()
with Session(db.engine) as session, session.begin():
with sessionmaker(db.engine).begin() as session:
service = MCPToolManageService(session=session)
service.delete_provider(tenant_id=current_tenant_id, provider_id=payload.provider_id)
@@ -1079,7 +1079,7 @@ class ToolMCPAuthApi(Resource):
provider_id = payload.provider_id
_, tenant_id = current_account_with_tenant()
with Session(db.engine) as session, session.begin():
with sessionmaker(db.engine).begin() as session:
service = MCPToolManageService(session=session)
db_provider = service.get_provider(provider_id=provider_id, tenant_id=tenant_id)
if not db_provider:
@@ -1100,7 +1100,7 @@ class ToolMCPAuthApi(Resource):
sse_read_timeout=provider_entity.sse_read_timeout,
):
# Update credentials in new transaction
with Session(db.engine) as session, session.begin():
with sessionmaker(db.engine).begin() as session:
service = MCPToolManageService(session=session)
service.update_provider_credentials(
provider_id=provider_id,
@@ -1118,17 +1118,17 @@ class ToolMCPAuthApi(Resource):
resource_metadata_url=e.resource_metadata_url,
scope_hint=e.scope_hint,
)
with Session(db.engine) as session, session.begin():
with sessionmaker(db.engine).begin() as session:
service = MCPToolManageService(session=session)
response = service.execute_auth_actions(auth_result)
return response
except MCPRefreshTokenError as e:
with Session(db.engine) as session, session.begin():
with sessionmaker(db.engine).begin() as session:
service = MCPToolManageService(session=session)
service.clear_provider_credentials(provider_id=provider_id, tenant_id=tenant_id)
raise ValueError(f"Failed to refresh token, please try to authorize again: {e}") from e
except (MCPError, ValueError) as e:
with Session(db.engine) as session, session.begin():
with sessionmaker(db.engine).begin() as session:
service = MCPToolManageService(session=session)
service.clear_provider_credentials(provider_id=provider_id, tenant_id=tenant_id)
raise ValueError(f"Failed to connect to MCP server: {e}") from e
@@ -1141,7 +1141,7 @@ class ToolMCPDetailApi(Resource):
@account_initialization_required
def get(self, provider_id):
_, tenant_id = current_account_with_tenant()
with Session(db.engine) as session, session.begin():
with sessionmaker(db.engine).begin() as session:
service = MCPToolManageService(session=session)
provider = service.get_provider(provider_id=provider_id, tenant_id=tenant_id)
return jsonable_encoder(ToolTransformService.mcp_provider_to_user_provider(provider, for_list=True))
@@ -1155,7 +1155,7 @@ class ToolMCPListAllApi(Resource):
def get(self):
_, tenant_id = current_account_with_tenant()
with Session(db.engine) as session, session.begin():
with sessionmaker(db.engine).begin() as session:
service = MCPToolManageService(session=session)
# Skip sensitive data decryption for list view to improve performance
tools = service.list_providers(tenant_id=tenant_id, include_sensitive=False)
@@ -1170,7 +1170,7 @@ class ToolMCPUpdateApi(Resource):
@account_initialization_required
def get(self, provider_id):
_, tenant_id = current_account_with_tenant()
with Session(db.engine) as session, session.begin():
with sessionmaker(db.engine).begin() as session:
service = MCPToolManageService(session=session)
tools = service.list_provider_tools(
tenant_id=tenant_id,
@@ -1188,7 +1188,7 @@ class ToolMCPCallbackApi(Resource):
authorization_code = query.code
# Create service instance for handle_callback
with Session(db.engine) as session, session.begin():
with sessionmaker(db.engine).begin() as session:
mcp_service = MCPToolManageService(session=session)
# handle_callback now returns state data and tokens
state_data, tokens = handle_callback(state_key, authorization_code)

View File

@@ -5,7 +5,7 @@ from flask import make_response, redirect, request
from flask_restx import Resource
from graphon.model_runtime.utils.encoders import jsonable_encoder
from pydantic import BaseModel, model_validator
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import BadRequest, Forbidden
from configs import dify_config
@@ -375,7 +375,7 @@ class TriggerSubscriptionDeleteApi(Resource):
assert user.current_tenant_id is not None
try:
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
# Delete trigger provider subscription
TriggerProviderService.delete_trigger_provider(
session=session,
@@ -388,7 +388,6 @@ class TriggerSubscriptionDeleteApi(Resource):
tenant_id=user.current_tenant_id,
subscription_id=subscription_id,
)
session.commit()
return {"result": "success"}
except ValueError as e:
raise BadRequest(str(e))

View File

@@ -155,7 +155,7 @@ class WorkspaceListApi(Resource):
@setup_required
@admin_required
def get(self):
payload = request.args.to_dict(flat=True) # type: ignore
payload = request.args.to_dict(flat=True)
args = WorkspaceListQuery.model_validate(payload)
stmt = select(Tenant).order_by(Tenant.created_at.desc())

View File

@@ -4,7 +4,7 @@ from flask import Response
from flask_restx import Resource
from graphon.variables.input_entities import VariableEntity
from pydantic import BaseModel, Field, ValidationError
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session, sessionmaker
from controllers.common.schema import register_schema_model
from controllers.mcp import mcp_ns
@@ -67,7 +67,7 @@ class MCPAppApi(Resource):
request_id: Union[int, str] | None = args.id
mcp_request = self._parse_mcp_request(args.model_dump(exclude_none=True))
with Session(db.engine, expire_on_commit=False) as session:
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
# Get MCP server and app
mcp_server, app = self._get_mcp_server_and_app(server_code, session)
self._validate_server_status(mcp_server)
@@ -189,7 +189,7 @@ class MCPAppApi(Resource):
def _retrieve_end_user(self, tenant_id: str, mcp_server_id: str) -> EndUser | None:
"""Get end user - manages its own database session"""
with Session(db.engine, expire_on_commit=False) as session, session.begin():
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
return (
session.query(EndUser)
.where(EndUser.tenant_id == tenant_id)
@@ -229,9 +229,7 @@ class MCPAppApi(Resource):
if not end_user and isinstance(mcp_request.root, mcp_types.InitializeRequest):
client_info = mcp_request.root.params.clientInfo
client_name = f"{client_info.name}@{client_info.version}"
# Commit the session before creating end user to avoid transaction conflicts
session.commit()
with Session(db.engine, expire_on_commit=False) as create_session, create_session.begin():
with sessionmaker(db.engine, expire_on_commit=False).begin() as create_session:
end_user = self._create_end_user(client_name, app.tenant_id, app.id, mcp_server.id, create_session)
return handle_mcp_request(app, mcp_request, user_input_form, mcp_server, end_user, request_id)

View File

@@ -2,7 +2,7 @@ from typing import Literal
from flask import request
from pydantic import BaseModel, Field, TypeAdapter, field_validator, model_validator
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import NotFound
from controllers.common.schema import register_schema_models
@@ -99,7 +99,7 @@ class ConversationListApi(WebApiResource):
query = ConversationListQuery.model_validate(raw_args)
try:
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
pagination = WebConversationService.pagination_by_last_id(
session=session,
app_model=app_model,

View File

@@ -4,7 +4,7 @@ import secrets
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from controllers.common.schema import register_schema_models
from controllers.console.auth.error import (
@@ -81,7 +81,7 @@ class ForgotPasswordSendEmailApi(Resource):
else:
language = "en-US"
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
account = AccountService.get_account_by_email_with_case_fallback(request_email, session=session)
token = None
if account is None:
@@ -180,18 +180,17 @@ class ForgotPasswordResetApi(Resource):
email = reset_data.get("email", "")
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
account = AccountService.get_account_by_email_with_case_fallback(email, session=session)
if account:
self._update_existing_account(account, password_hashed, salt, session)
self._update_existing_account(account, password_hashed, salt)
else:
raise AuthenticationFailedError()
return {"result": "success"}
def _update_existing_account(self, account: Account, password_hashed, salt, session):
def _update_existing_account(self, account: Account, password_hashed, salt):
# Update existing account credentials
account.password = base64.b64encode(password_hashed).decode()
account.password_salt = base64.b64encode(salt).decode()
session.commit()

View File

@@ -6,7 +6,7 @@ from typing import Concatenate, ParamSpec, TypeVar
from flask import request
from flask_restx import Resource
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import BadRequest, NotFound, Unauthorized
from constants import HEADER_NAME_APP_CODE
@@ -49,7 +49,7 @@ def decode_jwt_token(app_code: str | None = None, user_id: str | None = None):
decoded = PassportService().verify(tk)
app_code = decoded.get("app_code")
app_id = decoded.get("app_id")
with Session(db.engine, expire_on_commit=False) as session:
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
app_model = session.scalar(select(App).where(App.id == app_id))
site = session.scalar(select(Site).where(Site.code == app_code))
if not app_model:

View File

@@ -383,14 +383,21 @@ class TestWorkflowAppLogEndpoints:
monkeypatch.setattr(workflow_app_log_module, "db", SimpleNamespace(engine=MagicMock()))
class DummySession:
class DummySessionCtx:
def __enter__(self):
return "session"
def __exit__(self, exc_type, exc, tb):
return False
monkeypatch.setattr(workflow_app_log_module, "Session", lambda *args, **kwargs: DummySession())
class DummySessionMaker:
def __init__(self, *args, **kwargs):
pass
def begin(self):
return DummySessionCtx()
monkeypatch.setattr(workflow_app_log_module, "sessionmaker", DummySessionMaker)
def fake_get_paginate(self, **_kwargs):
return {"items": [], "total": 0}
@@ -423,13 +430,20 @@ class TestWorkflowDraftVariableEndpoints:
monkeypatch.setattr(workflow_draft_variable_module, "db", SimpleNamespace(engine=MagicMock()))
monkeypatch.setattr(workflow_draft_variable_module, "current_user", SimpleNamespace(id="user-1"))
class DummySession:
class DummySessionCtx:
def __enter__(self):
return "session"
def __exit__(self, exc_type, exc, tb):
return False
class DummySessionMaker:
def __init__(self, *args, **kwargs):
pass
def begin(self):
return DummySessionCtx()
class DummyDraftService:
def __init__(self, session):
self.session = session
@@ -437,7 +451,7 @@ class TestWorkflowDraftVariableEndpoints:
def list_variables_without_values(self, **_kwargs):
return {"items": [], "total": 0}
monkeypatch.setattr(workflow_draft_variable_module, "Session", lambda *args, **kwargs: DummySession())
monkeypatch.setattr(workflow_draft_variable_module, "sessionmaker", DummySessionMaker)
class DummyWorkflowService:
def is_workflow_exist(self, *args, **kwargs):
@@ -543,14 +557,21 @@ class TestWorkflowTriggerEndpoints:
session = MagicMock()
session.query.return_value.where.return_value.first.return_value = trigger
class DummySession:
class DummySessionCtx:
def __enter__(self):
return session
def __exit__(self, exc_type, exc, tb):
return False
monkeypatch.setattr(workflow_trigger_module, "Session", lambda *_args, **_kwargs: DummySession())
class DummySessionMaker:
def __init__(self, *args, **kwargs):
pass
def begin(self):
return DummySessionCtx()
monkeypatch.setattr(workflow_trigger_module, "sessionmaker", DummySessionMaker)
with app.test_request_context("/?node_id=node-1"):
result = method(app_model=SimpleNamespace(id="app-1"))

View File

@@ -102,12 +102,12 @@ class TestDataSourceApi:
with (
app.test_request_context("/"),
patch("controllers.console.datasets.data_source.Session") as mock_session_class,
patch("controllers.console.datasets.data_source.sessionmaker") as mock_session_class,
patch("controllers.console.datasets.data_source.db.session.add"),
patch("controllers.console.datasets.data_source.db.session.commit"),
):
mock_session = MagicMock()
mock_session_class.return_value.__enter__.return_value = mock_session
mock_session_class.return_value.begin.return_value.__enter__.return_value = mock_session
mock_session.execute.return_value.scalar_one_or_none.return_value = binding
response, status = method(api, "b1", "enable")
@@ -123,12 +123,12 @@ class TestDataSourceApi:
with (
app.test_request_context("/"),
patch("controllers.console.datasets.data_source.Session") as mock_session_class,
patch("controllers.console.datasets.data_source.sessionmaker") as mock_session_class,
patch("controllers.console.datasets.data_source.db.session.add"),
patch("controllers.console.datasets.data_source.db.session.commit"),
):
mock_session = MagicMock()
mock_session_class.return_value.__enter__.return_value = mock_session
mock_session_class.return_value.begin.return_value.__enter__.return_value = mock_session
mock_session.execute.return_value.scalar_one_or_none.return_value = binding
response, status = method(api, "b1", "disable")
@@ -142,10 +142,10 @@ class TestDataSourceApi:
with (
app.test_request_context("/"),
patch("controllers.console.datasets.data_source.Session") as mock_session_class,
patch("controllers.console.datasets.data_source.sessionmaker") as mock_session_class,
):
mock_session = MagicMock()
mock_session_class.return_value.__enter__.return_value = mock_session
mock_session_class.return_value.begin.return_value.__enter__.return_value = mock_session
mock_session.execute.return_value.scalar_one_or_none.return_value = None
with pytest.raises(NotFound):
@@ -159,10 +159,10 @@ class TestDataSourceApi:
with (
app.test_request_context("/"),
patch("controllers.console.datasets.data_source.Session") as mock_session_class,
patch("controllers.console.datasets.data_source.sessionmaker") as mock_session_class,
):
mock_session = MagicMock()
mock_session_class.return_value.__enter__.return_value = mock_session
mock_session_class.return_value.begin.return_value.__enter__.return_value = mock_session
mock_session.execute.return_value.scalar_one_or_none.return_value = binding
with pytest.raises(ValueError):
@@ -176,10 +176,10 @@ class TestDataSourceApi:
with (
app.test_request_context("/"),
patch("controllers.console.datasets.data_source.Session") as mock_session_class,
patch("controllers.console.datasets.data_source.sessionmaker") as mock_session_class,
):
mock_session = MagicMock()
mock_session_class.return_value.__enter__.return_value = mock_session
mock_session_class.return_value.begin.return_value.__enter__.return_value = mock_session
mock_session.execute.return_value.scalar_one_or_none.return_value = binding
with pytest.raises(ValueError):
@@ -282,7 +282,7 @@ class TestDataSourceNotionListApi:
"controllers.console.datasets.data_source.DatasetService.get_dataset",
return_value=dataset,
),
patch("controllers.console.datasets.data_source.Session") as mock_session_class,
patch("controllers.console.datasets.data_source.sessionmaker") as mock_session_class,
patch(
"core.datasource.datasource_manager.DatasourceManager.get_datasource_runtime",
return_value=MagicMock(
@@ -292,7 +292,7 @@ class TestDataSourceNotionListApi:
),
):
mock_session = MagicMock()
mock_session_class.return_value.__enter__.return_value = mock_session
mock_session_class.return_value.begin.return_value.__enter__.return_value = mock_session
mock_session.scalars.return_value.all.return_value = [document]
response, status = method(api)
@@ -315,7 +315,7 @@ class TestDataSourceNotionListApi:
"controllers.console.datasets.data_source.DatasetService.get_dataset",
return_value=dataset,
),
patch("controllers.console.datasets.data_source.Session"),
patch("controllers.console.datasets.data_source.sessionmaker"),
):
with pytest.raises(ValueError):
method(api)

View File

@@ -0,0 +1,153 @@
"""Integration tests for console API key endpoints using testcontainers."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
from flask.testing import FlaskClient
from sqlalchemy import delete
from sqlalchemy.orm import Session
from models.enums import ApiTokenType
from models.model import ApiToken, App, AppMode
from tests.test_containers_integration_tests.controllers.console.helpers import (
authenticate_console_client,
create_console_account_and_tenant,
create_console_app,
)
@pytest.fixture
def setup_app(
db_session_with_containers: Session,
test_client_with_containers: FlaskClient,
) -> tuple[FlaskClient, dict[str, str], App]:
"""Create an authenticated client with an app for API key tests."""
account, tenant = create_console_account_and_tenant(db_session_with_containers)
app = create_console_app(db_session_with_containers, tenant.id, account.id, AppMode.CHAT)
headers = authenticate_console_client(test_client_with_containers, account)
return test_client_with_containers, headers, app
@pytest.fixture(autouse=True)
def cleanup_api_tokens(db_session_with_containers: Session):
"""Remove API tokens created during each test."""
yield
db_session_with_containers.execute(delete(ApiToken))
db_session_with_containers.commit()
class TestAppApiKeyListResource:
"""Tests for GET/POST /apps/<resource_id>/api-keys."""
def test_get_empty_keys(self, setup_app: tuple[FlaskClient, dict[str, str], App]) -> None:
client, headers, app = setup_app
resp = client.get(f"/console/api/apps/{app.id}/api-keys", headers=headers)
assert resp.status_code == 200
assert resp.json is not None
assert resp.json["data"] == []
def test_create_api_key(self, setup_app: tuple[FlaskClient, dict[str, str], App]) -> None:
client, headers, app = setup_app
resp = client.post(f"/console/api/apps/{app.id}/api-keys", headers=headers)
assert resp.status_code == 201
data = resp.json
assert data is not None
assert data["token"].startswith("app-")
assert data["id"] is not None
def test_get_keys_after_create(self, setup_app: tuple[FlaskClient, dict[str, str], App]) -> None:
client, headers, app = setup_app
client.post(f"/console/api/apps/{app.id}/api-keys", headers=headers)
client.post(f"/console/api/apps/{app.id}/api-keys", headers=headers)
resp = client.get(f"/console/api/apps/{app.id}/api-keys", headers=headers)
assert resp.status_code == 200
assert resp.json is not None
assert len(resp.json["data"]) == 2
def test_create_key_max_limit(
self,
setup_app: tuple[FlaskClient, dict[str, str], App],
db_session_with_containers: Session,
) -> None:
client, headers, app = setup_app
# Create 10 keys (the max)
for _ in range(10):
client.post(f"/console/api/apps/{app.id}/api-keys", headers=headers)
# 11th should fail
resp = client.post(f"/console/api/apps/{app.id}/api-keys", headers=headers)
assert resp.status_code == 400
def test_get_keys_for_nonexistent_app(
self,
setup_app: tuple[FlaskClient, dict[str, str], App],
) -> None:
client, headers, _ = setup_app
resp = client.get(
"/console/api/apps/00000000-0000-0000-0000-000000000000/api-keys",
headers=headers,
)
assert resp.status_code == 404
class TestAppApiKeyResource:
"""Tests for DELETE /apps/<resource_id>/api-keys/<api_key_id>."""
def test_delete_key_success(self, setup_app: tuple[FlaskClient, dict[str, str], App]) -> None:
client, headers, app = setup_app
create_resp = client.post(f"/console/api/apps/{app.id}/api-keys", headers=headers)
assert create_resp.json is not None
key_id = create_resp.json["id"]
resp = client.delete(f"/console/api/apps/{app.id}/api-keys/{key_id}", headers=headers)
assert resp.status_code == 204
def test_delete_nonexistent_key(self, setup_app: tuple[FlaskClient, dict[str, str], App]) -> None:
client, headers, app = setup_app
resp = client.delete(
f"/console/api/apps/{app.id}/api-keys/00000000-0000-0000-0000-000000000000",
headers=headers,
)
assert resp.status_code == 404
def test_delete_key_nonexistent_app(
self,
setup_app: tuple[FlaskClient, dict[str, str], App],
) -> None:
client, headers, _ = setup_app
resp = client.delete(
"/console/api/apps/00000000-0000-0000-0000-000000000000/api-keys/00000000-0000-0000-0000-000000000000",
headers=headers,
)
assert resp.status_code == 404
def test_delete_forbidden_for_non_admin(
self,
flask_app_with_containers,
) -> None:
"""A non-admin member cannot delete API keys via the controller permission check."""
from werkzeug.exceptions import Forbidden
from controllers.console.apikey import BaseApiKeyResource
resource = BaseApiKeyResource()
resource.resource_type = ApiTokenType.APP
resource.resource_model = MagicMock()
resource.resource_id_field = "app_id"
non_admin = MagicMock()
non_admin.is_admin_or_owner = False
with (
flask_app_with_containers.test_request_context("/"),
patch(
"controllers.console.apikey.current_account_with_tenant",
return_value=(non_admin, "tenant-id"),
),
patch("controllers.console.apikey._get_resource"),
):
with pytest.raises(Forbidden):
BaseApiKeyResource.delete(resource, "rid", "kid")

View File

@@ -69,7 +69,7 @@ def client(flask_app_with_containers):
return_value=(MagicMock(id="u1"), "t1"),
autospec=True,
)
@patch("controllers.console.workspace.tool_providers.Session", autospec=True)
@patch("controllers.console.workspace.tool_providers.sessionmaker", autospec=True)
@patch("controllers.console.workspace.tool_providers.MCPToolManageService._reconnect_with_url", autospec=True)
@pytest.mark.usefixtures("_mock_cache", "_mock_user_tenant")
def test_create_mcp_provider_populates_tools(mock_reconnect, mock_session, mock_current_account_with_tenant, client):
@@ -88,7 +88,7 @@ def test_create_mcp_provider_populates_tools(mock_reconnect, mock_session, mock_
create_result.id = "provider-1"
svc.create_provider.return_value = create_result
svc.get_provider.return_value = MagicMock(id="provider-1", tenant_id="t1") # used by reload path
mock_session.return_value.__enter__.return_value = MagicMock()
mock_session.return_value.begin.return_value.__enter__.return_value = MagicMock()
# Patch MCPToolManageService constructed inside controller
with patch("controllers.console.workspace.tool_providers.MCPToolManageService", return_value=svc, autospec=True):
payload = {

View File

@@ -306,14 +306,14 @@ class TestTriggerSubscriptionCrud:
app.test_request_context("/"),
patch("controllers.console.workspace.trigger_providers.current_user", mock_user()),
patch("controllers.console.workspace.trigger_providers.db") as mock_db,
patch("controllers.console.workspace.trigger_providers.Session") as mock_session_cls,
patch("controllers.console.workspace.trigger_providers.sessionmaker") as mock_session_cls,
patch("controllers.console.workspace.trigger_providers.TriggerProviderService.delete_trigger_provider"),
patch(
"controllers.console.workspace.trigger_providers.TriggerSubscriptionOperatorService.delete_plugin_trigger_by_subscription"
),
):
mock_db.engine = MagicMock()
mock_session_cls.return_value.__enter__.return_value = mock_session
mock_session_cls.return_value.begin.return_value.__enter__.return_value = mock_session
result = method(api, "sub1")
@@ -327,14 +327,14 @@ class TestTriggerSubscriptionCrud:
app.test_request_context("/"),
patch("controllers.console.workspace.trigger_providers.current_user", mock_user()),
patch("controllers.console.workspace.trigger_providers.db") as mock_db,
patch("controllers.console.workspace.trigger_providers.Session") as session_cls,
patch("controllers.console.workspace.trigger_providers.sessionmaker") as session_cls,
patch(
"controllers.console.workspace.trigger_providers.TriggerProviderService.delete_trigger_provider",
side_effect=ValueError("bad"),
),
):
mock_db.engine = MagicMock()
session_cls.return_value.__enter__.return_value = MagicMock()
session_cls.return_value.begin.return_value.__enter__.return_value = MagicMock()
with pytest.raises(BadRequest):
method(api, "sub1")

View File

@@ -37,7 +37,7 @@ class TestForgotPasswordSendEmailApi:
@patch("controllers.web.forgot_password.AccountService.get_account_by_email_with_case_fallback")
@patch("controllers.web.forgot_password.AccountService.is_email_send_ip_limit", return_value=False)
@patch("controllers.web.forgot_password.extract_remote_ip", return_value="127.0.0.1")
@patch("controllers.web.forgot_password.Session")
@patch("controllers.web.forgot_password.sessionmaker")
def test_should_normalize_email_before_sending(
self,
mock_session_cls,
@@ -51,7 +51,7 @@ class TestForgotPasswordSendEmailApi:
mock_get_account.return_value = mock_account
mock_send_mail.return_value = "token-123"
mock_session = MagicMock()
mock_session_cls.return_value.__enter__.return_value = mock_session
mock_session_cls.return_value.begin.return_value.__enter__.return_value = mock_session
with patch("controllers.web.forgot_password.db", SimpleNamespace(engine="engine")):
with app.test_request_context(
@@ -153,7 +153,7 @@ class TestForgotPasswordResetApi:
@patch("controllers.web.forgot_password.ForgotPasswordResetApi._update_existing_account")
@patch("controllers.web.forgot_password.AccountService.get_account_by_email_with_case_fallback")
@patch("controllers.web.forgot_password.Session")
@patch("controllers.web.forgot_password.sessionmaker")
@patch("controllers.web.forgot_password.AccountService.revoke_reset_password_token")
@patch("controllers.web.forgot_password.AccountService.get_reset_password_data")
def test_should_fetch_account_with_fallback(
@@ -169,7 +169,7 @@ class TestForgotPasswordResetApi:
mock_account = MagicMock()
mock_get_account.return_value = mock_account
mock_session = MagicMock()
mock_session_cls.return_value.__enter__.return_value = mock_session
mock_session_cls.return_value.begin.return_value.__enter__.return_value = mock_session
with patch("controllers.web.forgot_password.db", SimpleNamespace(engine="engine")):
with app.test_request_context(
@@ -190,7 +190,7 @@ class TestForgotPasswordResetApi:
@patch("controllers.web.forgot_password.hash_password", return_value=b"hashed-value")
@patch("controllers.web.forgot_password.secrets.token_bytes", return_value=b"0123456789abcdef")
@patch("controllers.web.forgot_password.Session")
@patch("controllers.web.forgot_password.sessionmaker")
@patch("controllers.web.forgot_password.AccountService.revoke_reset_password_token")
@patch("controllers.web.forgot_password.AccountService.get_reset_password_data")
@patch("controllers.web.forgot_password.AccountService.get_account_by_email_with_case_fallback")
@@ -208,7 +208,7 @@ class TestForgotPasswordResetApi:
account = MagicMock()
mock_get_account.return_value = account
mock_session = MagicMock()
mock_session_cls.return_value.__enter__.return_value = mock_session
mock_session_cls.return_value.begin.return_value.__enter__.return_value = mock_session
with patch("controllers.web.forgot_password.db", SimpleNamespace(engine="engine")):
with app.test_request_context(
@@ -231,4 +231,3 @@ class TestForgotPasswordResetApi:
assert account.password == expected_password
expected_salt = base64.b64encode(b"0123456789abcdef").decode()
assert account.password_salt == expected_salt
mock_session.commit.assert_called_once()

View File

@@ -1,139 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from werkzeug.exceptions import Forbidden
from controllers.console.apikey import (
BaseApiKeyListResource,
BaseApiKeyResource,
_get_resource,
)
from models.enums import ApiTokenType
@pytest.fixture
def tenant_context_admin():
with patch("controllers.console.apikey.current_account_with_tenant") as mock:
user = MagicMock()
user.is_admin_or_owner = True
mock.return_value = (user, "tenant-123")
yield mock
@pytest.fixture
def tenant_context_non_admin():
with patch("controllers.console.apikey.current_account_with_tenant") as mock:
user = MagicMock()
user.is_admin_or_owner = False
mock.return_value = (user, "tenant-123")
yield mock
@pytest.fixture
def db_mock():
with patch("controllers.console.apikey.db") as mock_db:
mock_db.session = MagicMock()
yield mock_db
@pytest.fixture(autouse=True)
def bypass_permissions():
with patch(
"controllers.console.apikey.edit_permission_required",
lambda f: f,
):
yield
class DummyApiKeyListResource(BaseApiKeyListResource):
resource_type = ApiTokenType.APP
resource_model = MagicMock()
resource_id_field = "app_id"
token_prefix = "app-"
class DummyApiKeyResource(BaseApiKeyResource):
resource_type = ApiTokenType.APP
resource_model = MagicMock()
resource_id_field = "app_id"
class TestGetResource:
def test_get_resource_success(self):
fake_resource = MagicMock()
with (
patch("controllers.console.apikey.select") as mock_select,
patch("controllers.console.apikey.Session") as mock_session,
patch("controllers.console.apikey.db") as mock_db,
):
mock_db.engine = MagicMock()
mock_select.return_value.filter_by.return_value = MagicMock()
session = mock_session.return_value.__enter__.return_value
session.execute.return_value.scalar_one_or_none.return_value = fake_resource
result = _get_resource("rid", "tid", MagicMock)
assert result == fake_resource
def test_get_resource_not_found(self):
with (
patch("controllers.console.apikey.select") as mock_select,
patch("controllers.console.apikey.Session") as mock_session,
patch("controllers.console.apikey.db") as mock_db,
patch("controllers.console.apikey.flask_restx.abort") as abort,
):
mock_db.engine = MagicMock()
mock_select.return_value.filter_by.return_value = MagicMock()
session = mock_session.return_value.__enter__.return_value
session.execute.return_value.scalar_one_or_none.return_value = None
_get_resource("rid", "tid", MagicMock)
abort.assert_called_once()
class TestBaseApiKeyListResource:
def test_get_apikeys_success(self, tenant_context_admin, db_mock):
resource = DummyApiKeyListResource()
with patch("controllers.console.apikey._get_resource"):
db_mock.session.scalars.return_value.all.return_value = [MagicMock(), MagicMock()]
result = DummyApiKeyListResource.get.__wrapped__(resource, "resource-id")
assert "items" in result
class TestBaseApiKeyResource:
def test_delete_forbidden(self, tenant_context_non_admin, db_mock):
resource = DummyApiKeyResource()
with patch("controllers.console.apikey._get_resource"):
with pytest.raises(Forbidden):
DummyApiKeyResource.delete(resource, "rid", "kid")
def test_delete_key_not_found(self, tenant_context_admin, db_mock):
resource = DummyApiKeyResource()
db_mock.session.scalar.return_value = None
with patch("controllers.console.apikey._get_resource"):
with pytest.raises(Exception) as exc_info:
DummyApiKeyResource.delete(resource, "rid", "kid")
# flask_restx.abort raises HTTPException with message in data attribute
assert exc_info.value.data["message"] == "API key not found"
def test_delete_success(self, tenant_context_admin, db_mock):
resource = DummyApiKeyResource()
db_mock.session.scalar.return_value = MagicMock()
with (
patch("controllers.console.apikey._get_resource"),
patch("controllers.console.apikey.ApiTokenCache.delete"),
):
result, status = DummyApiKeyResource.delete(resource, "rid", "kid")
assert status == 204
assert result == {"result": "success"}
db_mock.session.commit.assert_called_once()

View File

@@ -162,6 +162,15 @@
"environment.development": "تطوير",
"environment.testing": "اختبار",
"error": "خطأ",
"errorBoundary.componentStack": "مكدس المكون:",
"errorBoundary.details": "تفاصيل الخطأ (التطوير فقط)",
"errorBoundary.errorCount": "حدث هذا الخطأ {{count}} مرة",
"errorBoundary.fallbackTitle": "عذراً! حدث خطأ ما",
"errorBoundary.message": "حدث خطأ غير متوقع أثناء عرض هذا المكون.",
"errorBoundary.reloadPage": "إعادة تحميل الصفحة",
"errorBoundary.title": "حدث خطأ ما",
"errorBoundary.tryAgain": "حاول مجدداً",
"errorBoundary.tryAgainCompact": "حاول مجدداً",
"errorMsg.fieldRequired": "{{field}} مطلوب",
"errorMsg.urlError": "يجب أن يبدأ العنوان بـ http:// أو https://",
"feedback.content": "محتوى التعليق",

View File

@@ -287,7 +287,10 @@
"env.export.checkbox": "تصدير القيم السرية",
"env.export.export": "تصدير DSL مع القيم السرية ",
"env.export.ignore": "تصدير DSL",
"env.export.name": "الاسم",
"env.export.secret": "سري",
"env.export.title": "تصدير متغيرات البيئة السرية؟",
"env.export.value": "القيمة",
"env.modal.description": "الوصف",
"env.modal.descriptionPlaceholder": "وصف المتغير",
"env.modal.editTitle": "تعديل متغير بيئة",

View File

@@ -162,6 +162,15 @@
"environment.development": "ENTWICKLUNG",
"environment.testing": "TESTEN",
"error": "Fehler",
"errorBoundary.componentStack": "Komponenten-Stack:",
"errorBoundary.details": "Fehlerdetails (Nur Entwicklung)",
"errorBoundary.errorCount": "Dieser Fehler ist {{count}} Mal aufgetreten",
"errorBoundary.fallbackTitle": "Hoppla! Etwas ist schiefgelaufen",
"errorBoundary.message": "Beim Rendern dieser Komponente ist ein unerwarteter Fehler aufgetreten.",
"errorBoundary.reloadPage": "Seite neu laden",
"errorBoundary.title": "Etwas ist schiefgelaufen",
"errorBoundary.tryAgain": "Erneut versuchen",
"errorBoundary.tryAgainCompact": "Erneut versuchen",
"errorMsg.fieldRequired": "{{field}} ist erforderlich",
"errorMsg.urlError": "Die URL sollte mit http:// oder https:// beginnen",
"feedback.content": "Feedback-Inhalt",

View File

@@ -287,7 +287,10 @@
"env.export.checkbox": "Geheime Werte exportieren",
"env.export.export": "DSL mit geheimen Werten exportieren",
"env.export.ignore": "DSL exportieren",
"env.export.name": "Name",
"env.export.secret": "Geheim",
"env.export.title": "Geheime Umgebungsvariablen exportieren?",
"env.export.value": "Wert",
"env.modal.description": "Beschreibung",
"env.modal.descriptionPlaceholder": "Beschreiben Sie die Variable",
"env.modal.editTitle": "Umgebungsvariable bearbeiten",

View File

@@ -162,6 +162,15 @@
"environment.development": "DESARROLLO",
"environment.testing": "PRUEBAS",
"error": "Error",
"errorBoundary.componentStack": "Pila de Componentes:",
"errorBoundary.details": "Detalles del Error (Solo Desarrollo)",
"errorBoundary.errorCount": "Este error ha ocurrido {{count}} veces",
"errorBoundary.fallbackTitle": "¡Vaya! Algo salió mal",
"errorBoundary.message": "Ocurrió un error inesperado al renderizar este componente.",
"errorBoundary.reloadPage": "Recargar Página",
"errorBoundary.title": "Algo salió mal",
"errorBoundary.tryAgain": "Intentar de Nuevo",
"errorBoundary.tryAgainCompact": "Intentar de nuevo",
"errorMsg.fieldRequired": "{{field}} es requerido",
"errorMsg.urlError": "la URL debe comenzar con http:// o https://",
"feedback.content": "Contenido de retroalimentación",

View File

@@ -287,7 +287,10 @@
"env.export.checkbox": "Exportar valores secretos",
"env.export.export": "Exportar DSL con valores secretos",
"env.export.ignore": "Exportar DSL",
"env.export.name": "Nombre",
"env.export.secret": "Secreto",
"env.export.title": "¿Exportar variables de entorno secretas?",
"env.export.value": "Valor",
"env.modal.description": "Descripción",
"env.modal.descriptionPlaceholder": "Describa la variable",
"env.modal.editTitle": "Editar Variable de Entorno",

View File

@@ -162,6 +162,15 @@
"environment.development": "توسعه",
"environment.testing": "آزمایشی",
"error": "خطا",
"errorBoundary.componentStack": "پشته کامپوننت:",
"errorBoundary.details": "جزئیات خطا (فقط در محیط توسعه)",
"errorBoundary.errorCount": "این خطا {{count}} بار رخ داده است",
"errorBoundary.fallbackTitle": "اوه! مشکلی پیش آمد",
"errorBoundary.message": "هنگام رندر کردن این کامپوننت، یک خطای غیرمنتظره رخ داد.",
"errorBoundary.reloadPage": "بارگذاری مجدد صفحه",
"errorBoundary.title": "مشکلی پیش آمد",
"errorBoundary.tryAgain": "تلاش مجدد",
"errorBoundary.tryAgainCompact": "تلاش مجدد",
"errorMsg.fieldRequired": "{{field}} الزامی است",
"errorMsg.urlError": "آدرس باید با http:// یا https:// شروع شود",
"feedback.content": "محتوای بازخورد",

View File

@@ -287,7 +287,10 @@
"env.export.checkbox": "خروجی مقادیر محرمانه",
"env.export.export": "خروجی DSL با مقادیر محرمانه",
"env.export.ignore": "خروجی DSL",
"env.export.name": "نام",
"env.export.secret": "محرمانه",
"env.export.title": "آیا متغیرهای محیطی محرمانه صادر شوند؟",
"env.export.value": "مقدار",
"env.modal.description": "توضیحات",
"env.modal.descriptionPlaceholder": "توصیف متغیر",
"env.modal.editTitle": "ویرایش متغیر محیطی",

View File

@@ -162,6 +162,15 @@
"environment.development": "DÉVELOPPEMENT",
"environment.testing": "TESTER",
"error": "Erreur",
"errorBoundary.componentStack": "Pile du Composant :",
"errorBoundary.details": "Détails de l'Erreur (Développement Uniquement)",
"errorBoundary.errorCount": "Cette erreur s'est produite {{count}} fois",
"errorBoundary.fallbackTitle": "Oups ! Quelque chose s'est mal passé",
"errorBoundary.message": "Une erreur inattendue s'est produite lors du rendu de ce composant.",
"errorBoundary.reloadPage": "Recharger la Page",
"errorBoundary.title": "Quelque chose s'est mal passé",
"errorBoundary.tryAgain": "Réessayer",
"errorBoundary.tryAgainCompact": "Réessayer",
"errorMsg.fieldRequired": "{{field}} est obligatoire",
"errorMsg.urlError": "LURL doit commencer par http:// ou https://",
"feedback.content": "Contenu des retours",

View File

@@ -287,7 +287,10 @@
"env.export.checkbox": "Exporter les valeurs secrètes",
"env.export.export": "Exporter les DSL avec des valeurs secrètes",
"env.export.ignore": "Exporter DSL",
"env.export.name": "Nom",
"env.export.secret": "Secret",
"env.export.title": "Exporter des variables d'environnement secrètes?",
"env.export.value": "valeur",
"env.modal.description": "Description",
"env.modal.descriptionPlaceholder": "Décrivez la variable",
"env.modal.editTitle": "Editer titre",

View File

@@ -162,6 +162,15 @@
"environment.development": "विकास",
"environment.testing": "परीक्षण",
"error": "त्रुटि",
"errorBoundary.componentStack": "कंपोनेंट स्टैक:",
"errorBoundary.details": "त्रुटि विवरण (केवल डेवलपमेंट)",
"errorBoundary.errorCount": "यह त्रुटि {{count}} बार हुई है",
"errorBoundary.fallbackTitle": "उफ़! कुछ गलत हो गया",
"errorBoundary.message": "इस कंपोनेंट को रेंडर करते समय एक अप्रत्याशित त्रुटि हुई।",
"errorBoundary.reloadPage": "पेज रीलोड करें",
"errorBoundary.title": "कुछ गलत हो गया",
"errorBoundary.tryAgain": "पुनः प्रयास करें",
"errorBoundary.tryAgainCompact": "पुनः प्रयास करें",
"errorMsg.fieldRequired": "{{field}} आवश्यक है",
"errorMsg.urlError": "url को http:// या https:// से शुरू होना चाहिए",
"feedback.content": "प्रतिक्रिया सामग्री",

View File

@@ -287,7 +287,10 @@
"env.export.checkbox": "गुप्त मान निर्यात करें",
"env.export.export": "गुप्त मानों के साथ DSL निर्यात करें",
"env.export.ignore": "DSL निर्यात करें",
"env.export.name": "नाम",
"env.export.secret": "गुप्त",
"env.export.title": "गुप्त पर्यावरण चर निर्यात करें?",
"env.export.value": "मान",
"env.modal.description": "विवरण",
"env.modal.descriptionPlaceholder": "चर का वर्णन करें",
"env.modal.editTitle": "पर्यावरण चर संपादित करें",

View File

@@ -162,6 +162,15 @@
"environment.development": "PENGEMBANGAN",
"environment.testing": "PENGUJIAN",
"error": "Kesalahan",
"errorBoundary.componentStack": "Tumpukan Komponen:",
"errorBoundary.details": "Detail Kesalahan (Hanya Pengembangan)",
"errorBoundary.errorCount": "Kesalahan ini telah terjadi {{count}} kali",
"errorBoundary.fallbackTitle": "Ups! Ada yang salah",
"errorBoundary.message": "Terjadi kesalahan tak terduga saat merender komponen ini.",
"errorBoundary.reloadPage": "Muat Ulang Halaman",
"errorBoundary.title": "Ada yang salah",
"errorBoundary.tryAgain": "Coba Lagi",
"errorBoundary.tryAgainCompact": "Coba lagi",
"errorMsg.fieldRequired": "{{field}} wajib diisi",
"errorMsg.urlError": "URL harus dimulai dengan http:// atau https://",
"feedback.content": "Konten Umpan Balik",

View File

@@ -287,7 +287,10 @@
"env.export.checkbox": "Mengekspor nilai rahasia",
"env.export.export": "Mengekspor DSL dengan nilai rahasia",
"env.export.ignore": "Ekspor DSL",
"env.export.name": "Nama",
"env.export.secret": "Rahasia",
"env.export.title": "Mengekspor variabel lingkungan Rahasia?",
"env.export.value": "Nilai",
"env.modal.description": "Deskripsi",
"env.modal.descriptionPlaceholder": "Jelaskan variabel",
"env.modal.editTitle": "Edit Variabel Lingkungan",

View File

@@ -162,6 +162,15 @@
"environment.development": "SVILUPPO",
"environment.testing": "TEST",
"error": "Errore",
"errorBoundary.componentStack": "Stack del Componente:",
"errorBoundary.details": "Dettagli Errore (Solo Sviluppo)",
"errorBoundary.errorCount": "Questo errore si è verificato {{count}} volte",
"errorBoundary.fallbackTitle": "Ops! Qualcosa è andato storto",
"errorBoundary.message": "Si è verificato un errore imprevisto durante il rendering di questo componente.",
"errorBoundary.reloadPage": "Ricarica Pagina",
"errorBoundary.title": "Qualcosa è andato storto",
"errorBoundary.tryAgain": "Riprova",
"errorBoundary.tryAgainCompact": "Riprova",
"errorMsg.fieldRequired": "{{field}} è obbligatorio",
"errorMsg.urlError": "L'URL deve iniziare con http:// o https://",
"feedback.content": "Contenuto del feedback",

View File

@@ -287,7 +287,10 @@
"env.export.checkbox": "Esporta valori segreti",
"env.export.export": "Esporta DSL con valori segreti",
"env.export.ignore": "Esporta DSL",
"env.export.name": "Nome",
"env.export.secret": "Segreto",
"env.export.title": "Esportare variabili d'ambiente segrete?",
"env.export.value": "Valore",
"env.modal.description": "Descrizione",
"env.modal.descriptionPlaceholder": "Descrivi la variabile",
"env.modal.editTitle": "Modifica Variabile d'Ambiente",

View File

@@ -162,6 +162,15 @@
"environment.development": "開発",
"environment.testing": "テスト",
"error": "エラー",
"errorBoundary.componentStack": "コンポーネントスタック:",
"errorBoundary.details": "エラー詳細(開発環境のみ)",
"errorBoundary.errorCount": "このエラーは{{count}}回発生しました",
"errorBoundary.fallbackTitle": "おっと!問題が発生しました",
"errorBoundary.message": "このコンポーネントのレンダリング中に予期しないエラーが発生しました。",
"errorBoundary.reloadPage": "ページを再読み込み",
"errorBoundary.title": "問題が発生しました",
"errorBoundary.tryAgain": "再試行",
"errorBoundary.tryAgainCompact": "再試行",
"errorMsg.fieldRequired": "{{field}}は必要です",
"errorMsg.urlError": "URL は http:// または https:// で始まる必要があります",
"feedback.content": "フィードバックコンテンツ",

View File

@@ -287,7 +287,10 @@
"env.export.checkbox": "シークレット値を含む",
"env.export.export": "シークレット値付きでエクスポート",
"env.export.ignore": "DSL をエクスポート",
"env.export.name": "名前",
"env.export.secret": "シークレット",
"env.export.title": "シークレット環境変数をエクスポートしますか?",
"env.export.value": "値",
"env.modal.description": "説明",
"env.modal.descriptionPlaceholder": "変数の説明を入力",
"env.modal.editTitle": "環境変数を編集",

View File

@@ -162,6 +162,15 @@
"environment.development": "개발",
"environment.testing": "테스트",
"error": "오류",
"errorBoundary.componentStack": "컴포넌트 스택:",
"errorBoundary.details": "오류 세부 정보 (개발 환경 전용)",
"errorBoundary.errorCount": "이 오류가 {{count}}번 발생했습니다",
"errorBoundary.fallbackTitle": "이런! 문제가 발생했습니다",
"errorBoundary.message": "이 컴포넌트를 렌더링하는 동안 예기치 않은 오류가 발생했습니다.",
"errorBoundary.reloadPage": "페이지 새로고침",
"errorBoundary.title": "문제가 발생했습니다",
"errorBoundary.tryAgain": "다시 시도",
"errorBoundary.tryAgainCompact": "다시 시도",
"errorMsg.fieldRequired": "{{field}}는 필수입니다.",
"errorMsg.urlError": "URL 은 http:// 또는 https:// 로 시작해야 합니다.",
"feedback.content": "피드백 내용",

View File

@@ -287,7 +287,10 @@
"env.export.checkbox": "비밀 값 내보내기",
"env.export.export": "비밀 값이 포함된 DSL 내보내기",
"env.export.ignore": "DSL 내보내기",
"env.export.name": "이름",
"env.export.secret": "비밀",
"env.export.title": "비밀 환경 변수를 내보내시겠습니까?",
"env.export.value": "값",
"env.modal.description": "설명",
"env.modal.descriptionPlaceholder": "변수에 대해 설명하세요",
"env.modal.editTitle": "환경 변수 편집",

View File

@@ -162,6 +162,15 @@
"environment.development": "DEVELOPMENT",
"environment.testing": "TESTING",
"error": "Error",
"errorBoundary.componentStack": "Componentstack:",
"errorBoundary.details": "Foutdetails (Alleen Ontwikkeling)",
"errorBoundary.errorCount": "Deze fout is {{count}} keer opgetreden",
"errorBoundary.fallbackTitle": "Oeps! Er is iets fout gegaan",
"errorBoundary.message": "Er is een onverwachte fout opgetreden bij het renderen van dit component.",
"errorBoundary.reloadPage": "Pagina herladen",
"errorBoundary.title": "Er is iets fout gegaan",
"errorBoundary.tryAgain": "Opnieuw proberen",
"errorBoundary.tryAgainCompact": "Opnieuw proberen",
"errorMsg.fieldRequired": "{{field}} is required",
"errorMsg.urlError": "url should start with http:// or https://",
"feedback.content": "Feedback Content",

View File

@@ -287,7 +287,10 @@
"env.export.checkbox": "Export secret values",
"env.export.export": "Export DSL with secret values ",
"env.export.ignore": "Export DSL",
"env.export.name": "Naam",
"env.export.secret": "Geheim",
"env.export.title": "Export Secret environment variables?",
"env.export.value": "Waarde",
"env.modal.description": "Description",
"env.modal.descriptionPlaceholder": "Describe the variable",
"env.modal.editTitle": "Edit Environment Variable",

View File

@@ -162,6 +162,15 @@
"environment.development": "ROZWOJOWA",
"environment.testing": "TESTOWANIE",
"error": "Błąd",
"errorBoundary.componentStack": "Stos komponentów:",
"errorBoundary.details": "Szczegóły błędu (tylko tryb deweloperski)",
"errorBoundary.errorCount": "Ten błąd wystąpił {{count}} razy",
"errorBoundary.fallbackTitle": "Ups! Coś poszło nie tak",
"errorBoundary.message": "Wystąpił nieoczekiwany błąd podczas renderowania tego komponentu.",
"errorBoundary.reloadPage": "Odśwież stronę",
"errorBoundary.title": "Coś poszło nie tak",
"errorBoundary.tryAgain": "Spróbuj ponownie",
"errorBoundary.tryAgainCompact": "Spróbuj ponownie",
"errorMsg.fieldRequired": "{{field}} jest wymagane",
"errorMsg.urlError": "Adres URL powinien zaczynać się od http:// lub https://",
"feedback.content": "Treść opinii",

View File

@@ -287,7 +287,10 @@
"env.export.checkbox": "Eksportuj tajne wartości",
"env.export.export": "Eksportuj DSL z tajnymi wartościami",
"env.export.ignore": "Eksportuj DSL",
"env.export.name": "Nazwa",
"env.export.secret": "Tajny",
"env.export.title": "Eksportować tajne zmienne środowiskowe?",
"env.export.value": "Wartość",
"env.modal.description": "Opis",
"env.modal.descriptionPlaceholder": "Opisz zmienną",
"env.modal.editTitle": "Edytuj Zmienną Środowiskową",

View File

@@ -162,6 +162,15 @@
"environment.development": "DESENVOLVIMENTO",
"environment.testing": "TESTE",
"error": "Erro",
"errorBoundary.componentStack": "Stack do Componente:",
"errorBoundary.details": "Detalhes do Erro (Somente Desenvolvimento)",
"errorBoundary.errorCount": "Este erro ocorreu {{count}} vezes",
"errorBoundary.fallbackTitle": "Ops! Algo deu errado",
"errorBoundary.message": "Ocorreu um erro inesperado ao renderizar este componente.",
"errorBoundary.reloadPage": "Recarregar Página",
"errorBoundary.title": "Algo deu errado",
"errorBoundary.tryAgain": "Tentar Novamente",
"errorBoundary.tryAgainCompact": "Tentar novamente",
"errorMsg.fieldRequired": "{{field}} é obrigatório",
"errorMsg.urlError": "URL deve começar com http:// ou https://",
"feedback.content": "Conteúdo do feedback",

View File

@@ -287,7 +287,10 @@
"env.export.checkbox": "Exportar valores secretos",
"env.export.export": "Exportar DSL com valores secretos",
"env.export.ignore": "Exportar DSL",
"env.export.name": "Nome",
"env.export.secret": "Secreto",
"env.export.title": "Exportar variáveis de ambiente secretas?",
"env.export.value": "Valor",
"env.modal.description": "Descrição",
"env.modal.descriptionPlaceholder": "Descreva a variável",
"env.modal.editTitle": "Editar Variável de Ambiente",

View File

@@ -162,6 +162,15 @@
"environment.development": "DEZVOLTARE",
"environment.testing": "TESTARE",
"error": "Eroare",
"errorBoundary.componentStack": "Stiva componentelor:",
"errorBoundary.details": "Detalii eroare (Numai în dezvoltare)",
"errorBoundary.errorCount": "Această eroare a apărut de {{count}} ori",
"errorBoundary.fallbackTitle": "Ups! Ceva a mers prost",
"errorBoundary.message": "A apărut o eroare neașteptată la redarea acestei componente.",
"errorBoundary.reloadPage": "Reîncarcă pagina",
"errorBoundary.title": "Ceva a mers prost",
"errorBoundary.tryAgain": "Încearcă din nou",
"errorBoundary.tryAgainCompact": "Încearcă din nou",
"errorMsg.fieldRequired": "{{field}} este obligatoriu",
"errorMsg.urlError": "URL-ul ar trebui să înceapă cu http:// sau https://",
"feedback.content": "Conținut de feedback",

View File

@@ -287,7 +287,10 @@
"env.export.checkbox": "Exportă valori secrete",
"env.export.export": "Exportă DSL cu valori secrete",
"env.export.ignore": "Exportă DSL",
"env.export.name": "Nume",
"env.export.secret": "Secret",
"env.export.title": "Exportă variabile de mediu secrete?",
"env.export.value": "Valoare",
"env.modal.description": "Descriere",
"env.modal.descriptionPlaceholder": "Descrieți variabila",
"env.modal.editTitle": "Editează Variabilă de Mediu",

View File

@@ -162,6 +162,15 @@
"environment.development": "РАЗРАБОТКА",
"environment.testing": "ТЕСТИРОВАНИЕ",
"error": "Ошибка",
"errorBoundary.componentStack": "Стек компонентов:",
"errorBoundary.details": "Детали ошибки (только разработка)",
"errorBoundary.errorCount": "Эта ошибка произошла {{count}} раз(а)",
"errorBoundary.fallbackTitle": "Упс! Что-то пошло не так",
"errorBoundary.message": "При рендеринге этого компонента произошла непредвиденная ошибка.",
"errorBoundary.reloadPage": "Перезагрузить страницу",
"errorBoundary.title": "Что-то пошло не так",
"errorBoundary.tryAgain": "Попробовать снова",
"errorBoundary.tryAgainCompact": "Попробовать снова",
"errorMsg.fieldRequired": "{{field}} обязательно",
"errorMsg.urlError": "URL должен начинаться с http:// или https://",
"feedback.content": "Содержимое обратной связи",

View File

@@ -287,7 +287,10 @@
"env.export.checkbox": "Экспортировать секретные значения",
"env.export.export": "Экспортировать DSL с секретными значениями ",
"env.export.ignore": "Экспортировать DSL",
"env.export.name": "Имя",
"env.export.secret": "Секрет",
"env.export.title": "Экспортировать секретные переменные среды?",
"env.export.value": "Значение",
"env.modal.description": "Описание",
"env.modal.descriptionPlaceholder": "Опишите переменную",
"env.modal.editTitle": "Редактировать переменную среды",

View File

@@ -162,6 +162,15 @@
"environment.development": "RAZVOJ",
"environment.testing": "PREIZKUŠANJE",
"error": "Napaka",
"errorBoundary.componentStack": "Sklad komponent:",
"errorBoundary.details": "Podrobnosti napake (samo razvojna okolja)",
"errorBoundary.errorCount": "Ta napaka se je pojavila {{count}} krat",
"errorBoundary.fallbackTitle": "Ojoj! Nekaj je šlo narobe",
"errorBoundary.message": "Med prikazovanjem te komponente je prišlo do nepričakovane napake.",
"errorBoundary.reloadPage": "Znova naloži stran",
"errorBoundary.title": "Nekaj je šlo narobe",
"errorBoundary.tryAgain": "Poskusi znova",
"errorBoundary.tryAgainCompact": "Poskusi znova",
"errorMsg.fieldRequired": "{{field}} je obvezno",
"errorMsg.urlError": "url mora začeti z http:// ali https://",
"feedback.content": "Vsebina povratnih informacij",

View File

@@ -287,7 +287,10 @@
"env.export.checkbox": "Izvozi tajne vrednosti",
"env.export.export": "Izvozi DSL z skrivnimi vrednostmi",
"env.export.ignore": "Izvoz DSL",
"env.export.name": "Ime",
"env.export.secret": "Skrivnost",
"env.export.title": "Izvozi skrivne okoljske spremenljivke?",
"env.export.value": "Vrednost",
"env.modal.description": "Opis",
"env.modal.descriptionPlaceholder": "Opisujte spremenljivko",
"env.modal.editTitle": "Uredi okoljsko spremenljivko",

View File

@@ -162,6 +162,15 @@
"environment.development": "พัฒนาการ",
"environment.testing": "การทดสอบ",
"error": "ข้อผิดพลาด",
"errorBoundary.componentStack": "สแตกของคอมโพเนนต์:",
"errorBoundary.details": "รายละเอียดข้อผิดพลาด (สำหรับการพัฒนาเท่านั้น)",
"errorBoundary.errorCount": "ข้อผิดพลาดนี้เกิดขึ้น {{count}} ครั้ง",
"errorBoundary.fallbackTitle": "อุ๊ปส์! มีบางอย่างผิดพลาด",
"errorBoundary.message": "เกิดข้อผิดพลาดที่ไม่คาดคิดขณะแสดงผลคอมโพเนนต์นี้",
"errorBoundary.reloadPage": "โหลดหน้าใหม่",
"errorBoundary.title": "มีบางอย่างผิดพลาด",
"errorBoundary.tryAgain": "ลองอีกครั้ง",
"errorBoundary.tryAgainCompact": "ลองอีกครั้ง",
"errorMsg.fieldRequired": "{{field}} เป็นสิ่งจําเป็น",
"errorMsg.urlError": "url ควรขึ้นต้นด้วย http:// หรือ https://",
"feedback.content": "เนื้อหาข้อเสนอแนะ",

View File

@@ -287,7 +287,10 @@
"env.export.checkbox": "ส่งออกค่าข้อมูลลับ",
"env.export.export": "ส่งออก DSL ด้วยค่าลับ",
"env.export.ignore": "ส่งออก DSL",
"env.export.name": "ชื่อ",
"env.export.secret": "Secret",
"env.export.title": "ส่งออกตัวแปรสภาพแวดล้อม Secret หรือไม่",
"env.export.value": "ค่า",
"env.modal.description": "คำอธิบาย",
"env.modal.descriptionPlaceholder": "อธิบายตัวแปร",
"env.modal.editTitle": "แก้ไขตัวแปรสภาพแวดล้อม",

View File

@@ -162,6 +162,15 @@
"environment.development": "GELİŞTİRME",
"environment.testing": "TEST",
"error": "Hata",
"errorBoundary.componentStack": "Bileşen Yığını:",
"errorBoundary.details": "Hata Ayrıntıları (Yalnızca Geliştirme)",
"errorBoundary.errorCount": "Bu hata {{count}} kez oluştu",
"errorBoundary.fallbackTitle": "Hay aksi! Bir şeyler ters gitti",
"errorBoundary.message": "Bu bileşen işlenirken beklenmedik bir hata oluştu.",
"errorBoundary.reloadPage": "Sayfayı Yenile",
"errorBoundary.title": "Bir şeyler ters gitti",
"errorBoundary.tryAgain": "Tekrar Dene",
"errorBoundary.tryAgainCompact": "Tekrar dene",
"errorMsg.fieldRequired": "{{field}} gereklidir",
"errorMsg.urlError": "URL http:// veya https:// ile başlamalıdır",
"feedback.content": "Geri Bildirim İçeriği",

View File

@@ -287,7 +287,10 @@
"env.export.checkbox": "Gizli değerleri dışa aktar",
"env.export.export": "Gizli değerlerle DSL'yi dışa aktar",
"env.export.ignore": "DSL'yi dışa aktar",
"env.export.name": "Ad",
"env.export.secret": "Gizli",
"env.export.title": "Gizli çevre değişkenleri dışa aktarılsın mı?",
"env.export.value": "Değer",
"env.modal.description": "Açıklama",
"env.modal.descriptionPlaceholder": "Değişkeni açıklayın",
"env.modal.editTitle": "Çevre Değişkenini Düzenle",

View File

@@ -162,6 +162,15 @@
"environment.development": "РОЗРОБКА",
"environment.testing": "ТЕСТУВАННЯ",
"error": "Помилка",
"errorBoundary.componentStack": "Стек компонентів:",
"errorBoundary.details": "Деталі помилки (тільки розробка)",
"errorBoundary.errorCount": "Ця помилка сталася {{count}} раз(ів)",
"errorBoundary.fallbackTitle": "Ой! Щось пішло не так",
"errorBoundary.message": "Під час відображення цього компонента сталася непередбачена помилка.",
"errorBoundary.reloadPage": "Перезавантажити сторінку",
"errorBoundary.title": "Щось пішло не так",
"errorBoundary.tryAgain": "Спробувати знову",
"errorBoundary.tryAgainCompact": "Спробувати знову",
"errorMsg.fieldRequired": "{{field}} є обов'язковим",
"errorMsg.urlError": "URL-адреса повинна починатися з http:// або https://",
"feedback.content": "Зміст відгуку",

View File

@@ -287,7 +287,10 @@
"env.export.checkbox": "Експортувати секретні значення",
"env.export.export": "Експортувати DSL з секретними значеннями",
"env.export.ignore": "Експортувати DSL",
"env.export.name": "Назва",
"env.export.secret": "Секрет",
"env.export.title": "Експортувати секретні змінні середовища?",
"env.export.value": "Значення",
"env.modal.description": "Опис",
"env.modal.descriptionPlaceholder": "Опишіть змінну",
"env.modal.editTitle": "Редагувати змінну середовища",

View File

@@ -162,6 +162,15 @@
"environment.development": "DEVELOPMENT",
"environment.testing": "TESTING",
"error": "Lỗi",
"errorBoundary.componentStack": "Ngăn xếp thành phần:",
"errorBoundary.details": "Chi tiết lỗi (Chỉ dành cho phát triển)",
"errorBoundary.errorCount": "Lỗi này đã xảy ra {{count}} lần",
"errorBoundary.fallbackTitle": "Ôi! Đã xảy ra sự cố",
"errorBoundary.message": "Đã xảy ra lỗi không mong muốn khi hiển thị thành phần này.",
"errorBoundary.reloadPage": "Tải lại trang",
"errorBoundary.title": "Đã xảy ra sự cố",
"errorBoundary.tryAgain": "Thử lại",
"errorBoundary.tryAgainCompact": "Thử lại",
"errorMsg.fieldRequired": "{{field}} là bắt buộc",
"errorMsg.urlError": "URL phải bắt đầu bằng http:// hoặc https://",
"feedback.content": "Nội dung phản hồi",

View File

@@ -287,7 +287,10 @@
"env.export.checkbox": "Xuất giá trị bí mật",
"env.export.export": "Xuất DSL với giá trị bí mật",
"env.export.ignore": "Xuất DSL",
"env.export.name": "Tên",
"env.export.secret": "Bí mật",
"env.export.title": "Xuất biến môi trường bí mật?",
"env.export.value": "Giá trị",
"env.modal.description": "Mô tả",
"env.modal.descriptionPlaceholder": "Mô tả biến",
"env.modal.editTitle": "Sửa Biến Môi Trường",

View File

@@ -164,6 +164,15 @@
"environment.development": "开发环境",
"environment.testing": "测试环境",
"error": "错误",
"errorBoundary.componentStack": "组件堆栈:",
"errorBoundary.details": "错误详情(仅开发模式)",
"errorBoundary.errorCount": "此错误已发生 {{count}} 次",
"errorBoundary.fallbackTitle": "哎呀!出了点问题",
"errorBoundary.message": "渲染此组件时发生了意外错误。",
"errorBoundary.reloadPage": "重新加载页面",
"errorBoundary.title": "出了点问题",
"errorBoundary.tryAgain": "重试",
"errorBoundary.tryAgainCompact": "重试",
"errorMsg.fieldRequired": "{{field}} 为必填项",
"errorMsg.urlError": "url 应该以 http:// 或 https:// 开头",
"feedback.content": "反馈内容",

View File

@@ -287,7 +287,10 @@
"env.export.checkbox": "导出 secret 值",
"env.export.export": "导出包含 Secret 值的 DSL",
"env.export.ignore": "导出 DSL",
"env.export.name": "名称",
"env.export.secret": "Secret",
"env.export.title": "导出 Secret 类型环境变量?",
"env.export.value": "值",
"env.modal.description": "描述",
"env.modal.descriptionPlaceholder": "变量的描述",
"env.modal.editTitle": "编辑环境变量",

View File

@@ -162,6 +162,15 @@
"environment.development": "開發環境",
"environment.testing": "測試環境",
"error": "錯誤",
"errorBoundary.componentStack": "元件堆疊:",
"errorBoundary.details": "錯誤詳情(僅開發模式)",
"errorBoundary.errorCount": "此錯誤已發生 {{count}} 次",
"errorBoundary.fallbackTitle": "哎呀!出了點問題",
"errorBoundary.message": "渲染此元件時發生了意外錯誤。",
"errorBoundary.reloadPage": "重新載入頁面",
"errorBoundary.title": "出了點問題",
"errorBoundary.tryAgain": "重試",
"errorBoundary.tryAgainCompact": "重試",
"errorMsg.fieldRequired": "{{field}} 為必填項",
"errorMsg.urlError": "URL 應以 http:// 或 https:// 開頭",
"feedback.content": "反饋內容",

View File

@@ -287,7 +287,10 @@
"env.export.checkbox": "導出機密值",
"env.export.export": "導出帶有機密值的 DSL",
"env.export.ignore": "導出 DSL",
"env.export.name": "名稱",
"env.export.secret": "機密",
"env.export.title": "導出機密環境變數?",
"env.export.value": "值",
"env.modal.description": "描述",
"env.modal.descriptionPlaceholder": "描述此變數",
"env.modal.editTitle": "編輯環境變數",