diff --git a/.github/labeler.yml b/.github/labeler.yml index d1d324d381..3b9dc24749 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,3 +1,10 @@ web: - changed-files: - - any-glob-to-any-file: 'web/**' + - any-glob-to-any-file: + - 'web/**' + - 'packages/**' + - 'package.json' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' + - '.npmrc' + - '.nvmrc' diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 50dbde2aee..a069b6cbc7 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -20,4 +20,4 @@ - [x] I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!) - [x] I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change. - [x] I've updated the documentation accordingly. -- [x] I ran `make lint` and `make type-check` (backend) and `cd web && npx lint-staged` (frontend) to appease the lint gods +- [x] I ran `make lint` and `make type-check` (backend) and `cd web && pnpm exec vp staged` (frontend) to appease the lint gods diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 9648c34274..772ab8dd56 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -39,9 +39,11 @@ jobs: with: files: | web/** + packages/** package.json pnpm-lock.yaml pnpm-workspace.yaml + .npmrc .nvmrc - name: Check api inputs if: github.event_name != 'merge_group' diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index a23edc70e5..79ecdb5938 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -65,7 +65,7 @@ jobs: echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - name: Login to Docker Hub - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ env.DOCKERHUB_USER }} password: ${{ env.DOCKERHUB_TOKEN }} @@ -130,7 +130,7 @@ jobs: merge-multiple: true - name: Login to Docker Hub - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ env.DOCKERHUB_USER }} password: ${{ env.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index cbeb1a3bb1..cd9d69d871 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -8,9 +8,11 @@ on: - api/Dockerfile - web/docker/** - web/Dockerfile + - packages/** - package.json - pnpm-lock.yaml - pnpm-workspace.yaml + - .npmrc - .nvmrc concurrency: diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index 104368d192..59c38b6e7e 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -65,9 +65,11 @@ jobs: - 'docker/volumes/sandbox/conf/**' web: - 'web/**' + - 'packages/**' - 'package.json' - 'pnpm-lock.yaml' - 'pnpm-workspace.yaml' + - '.npmrc' - '.nvmrc' - '.github/workflows/web-tests.yml' - '.github/actions/setup-web/**' @@ -77,9 +79,11 @@ jobs: - 'api/uv.lock' - 'e2e/**' - 'web/**' + - 'packages/**' - 'package.json' - 'pnpm-lock.yaml' - 'pnpm-workspace.yaml' + - '.npmrc' - '.nvmrc' - 'docker/docker-compose.middleware.yaml' - 'docker/middleware.env.example' diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index 9bc4ceaa93..c32fc9d0cb 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -77,9 +77,11 @@ jobs: with: files: | web/** + packages/** package.json pnpm-lock.yaml pnpm-workspace.yaml + .npmrc .nvmrc .github/workflows/style.yml .github/actions/setup-web/** @@ -149,7 +151,7 @@ jobs: .editorconfig - name: Super-linter - uses: super-linter/super-linter/slim@61abc07d755095a68f4987d1c2c3d1d64408f1f9 # v8.5.0 + uses: super-linter/super-linter/slim@9e863354e3ff62e0727d37183162c4a88873df41 # v8.6.0 if: steps.changed-files.outputs.any_changed == 'true' env: BASH_SEVERITY: warning diff --git a/.github/workflows/tool-test-sdks.yaml b/.github/workflows/tool-test-sdks.yaml index 536a52b560..467f31fccf 100644 --- a/.github/workflows/tool-test-sdks.yaml +++ b/.github/workflows/tool-test-sdks.yaml @@ -9,6 +9,7 @@ on: - package.json - pnpm-lock.yaml - pnpm-workspace.yaml + - .npmrc concurrency: group: sdk-tests-${{ github.head_ref || github.run_id }} diff --git a/.github/workflows/translate-i18n-claude.yml b/.github/workflows/translate-i18n-claude.yml index 33af4f36fd..a813c87cec 100644 --- a/.github/workflows/translate-i18n-claude.yml +++ b/.github/workflows/translate-i18n-claude.yml @@ -240,7 +240,7 @@ jobs: - name: Run Claude Code for Translation Sync if: steps.context.outputs.CHANGED_FILES != '' - uses: anthropics/claude-code-action@88c168b39e7e64da0286d812b6e9fbebb6708185 # v1.0.82 + uses: anthropics/claude-code-action@6e2bd52842c65e914eba5c8badd17560bd26b5de # v1.0.89 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/vdb-tests-full.yml b/.github/workflows/vdb-tests-full.yml index 01d25902f6..72b3ea9aac 100644 --- a/.github/workflows/vdb-tests-full.yml +++ b/.github/workflows/vdb-tests-full.yml @@ -36,7 +36,7 @@ jobs: remove_tool_cache: true - name: Setup UV and Python - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true python-version: ${{ matrix.python-version }} diff --git a/.vite-hooks/pre-commit b/.vite-hooks/pre-commit index 54e09f80d6..db5c504606 100755 --- a/.vite-hooks/pre-commit +++ b/.vite-hooks/pre-commit @@ -89,30 +89,10 @@ if $web_modified; then echo "No staged TypeScript changes detected, skipping type-check:tsgo" fi - echo "Running unit tests check" - modified_files=$(git diff --cached --name-only -- utils | grep -v '\.spec\.ts$' || true) - - if [ -n "$modified_files" ]; then - for file in $modified_files; do - test_file="${file%.*}.spec.ts" - echo "Checking for test file: $test_file" - - # check if the test file exists - if [ -f "../$test_file" ]; then - echo "Detected changes in $file, running corresponding unit tests..." - pnpm run test "../$test_file" - - if [ $? -ne 0 ]; then - echo "Unit tests failed. Please fix the errors before committing." - exit 1 - fi - echo "Unit tests for $file passed." - else - echo "Warning: $file does not have a corresponding test file." - fi - - done - echo "All unit tests for modified web/utils files have passed." + echo "Running knip" + if ! pnpm run knip; then + echo "Knip check failed. Please run 'pnpm run knip' to fix the errors." + exit 1 fi cd ../ diff --git a/api/celery_healthcheck.py b/api/celery_healthcheck.py new file mode 100644 index 0000000000..23d856d7d0 --- /dev/null +++ b/api/celery_healthcheck.py @@ -0,0 +1,18 @@ +# This module provides a lightweight Celery instance for use in Docker health checks. +# Unlike celery_entrypoint.py, this does NOT import app.py and therefore avoids +# initializing all Flask extensions (DB, Redis, storage, blueprints, etc.). +# Using this module keeps the health check fast and low-cost. +from celery import Celery + +from configs import dify_config +from extensions.ext_celery import get_celery_broker_transport_options, get_celery_ssl_options + +celery = Celery(broker=dify_config.CELERY_BROKER_URL) + +broker_transport_options = get_celery_broker_transport_options() +if broker_transport_options: + celery.conf.update(broker_transport_options=broker_transport_options) + +ssl_options = get_celery_ssl_options() +if ssl_options: + celery.conf.update(broker_use_ssl=ssl_options) diff --git a/api/commands/retention.py b/api/commands/retention.py index 82a77ea77a..657a2a2e83 100644 --- a/api/commands/retention.py +++ b/api/commands/retention.py @@ -1,7 +1,7 @@ import datetime import logging import time -from typing import Any +from typing import TypedDict import click import sqlalchemy as sa @@ -503,7 +503,19 @@ def _find_orphaned_draft_variables(batch_size: int = 1000) -> list[str]: return [row[0] for row in result] -def _count_orphaned_draft_variables() -> dict[str, Any]: +class _AppOrphanCounts(TypedDict): + variables: int + files: int + + +class OrphanedDraftVariableStatsDict(TypedDict): + total_orphaned_variables: int + total_orphaned_files: int + orphaned_app_count: int + orphaned_by_app: dict[str, _AppOrphanCounts] + + +def _count_orphaned_draft_variables() -> OrphanedDraftVariableStatsDict: """ Count orphaned draft variables by app, including associated file counts. @@ -526,7 +538,7 @@ def _count_orphaned_draft_variables() -> dict[str, Any]: with db.engine.connect() as conn: result = conn.execute(sa.text(variables_query)) - orphaned_by_app = {} + orphaned_by_app: dict[str, _AppOrphanCounts] = {} total_files = 0 for row in result: diff --git a/api/controllers/common/controller_schemas.py b/api/controllers/common/controller_schemas.py new file mode 100644 index 0000000000..e13bf025fc --- /dev/null +++ b/api/controllers/common/controller_schemas.py @@ -0,0 +1,63 @@ +from typing import Any, Literal + +from pydantic import BaseModel, Field, model_validator + +from libs.helper import UUIDStrOrEmpty + +# --- Conversation schemas --- + + +class ConversationRenamePayload(BaseModel): + name: str | None = None + auto_generate: bool = False + + @model_validator(mode="after") + def validate_name_requirement(self): + if not self.auto_generate: + if self.name is None or not self.name.strip(): + raise ValueError("name is required when auto_generate is false") + return self + + +# --- Message schemas --- + + +class MessageListQuery(BaseModel): + conversation_id: UUIDStrOrEmpty + first_id: UUIDStrOrEmpty | None = None + limit: int = Field(default=20, ge=1, le=100) + + +class MessageFeedbackPayload(BaseModel): + rating: Literal["like", "dislike"] | None = None + content: str | None = None + + +# --- Saved message schemas --- + + +class SavedMessageListQuery(BaseModel): + last_id: UUIDStrOrEmpty | None = None + limit: int = Field(default=20, ge=1, le=100) + + +class SavedMessageCreatePayload(BaseModel): + message_id: UUIDStrOrEmpty + + +# --- Workflow schemas --- + + +class WorkflowRunPayload(BaseModel): + inputs: dict[str, Any] + files: list[dict[str, Any]] | None = None + + +# --- Audio schemas --- + + +class TextToAudioPayload(BaseModel): + message_id: str | None = None + voice: str | None = None + text: str | None = None + streaming: bool | None = None diff --git a/api/controllers/console/admin.py b/api/controllers/console/admin.py index 9b8408980d..dce394be97 100644 --- a/api/controllers/console/admin.py +++ b/api/controllers/console/admin.py @@ -2,6 +2,7 @@ import csv import io from collections.abc import Callable from functools import wraps +from typing import cast from flask import request from flask_restx import Resource @@ -17,7 +18,7 @@ from core.db.session_factory import session_factory from extensions.ext_database import db from libs.token import extract_access_token from models.model import App, ExporleBanner, InstalledApp, RecommendedApp, TrialApp -from services.billing_service import BillingService +from services.billing_service import BillingService, LangContentDict DEFAULT_REF_TEMPLATE_SWAGGER_2_0 = "#/definitions/{model}" @@ -328,7 +329,7 @@ class UpsertNotificationApi(Resource): def post(self): payload = UpsertNotificationPayload.model_validate(console_ns.payload) result = BillingService.upsert_notification( - contents=[c.model_dump() for c in payload.contents], + contents=[cast(LangContentDict, c.model_dump()) for c in payload.contents], frequency=payload.frequency, status=payload.status, notification_id=payload.notification_id, diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index c67ca57c63..c4b9bf6540 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -7,7 +7,7 @@ from flask import request from flask_restx import Resource 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 pydantic import AliasChoices, BaseModel, Field, computed_field, field_validator from sqlalchemy import select from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest @@ -26,9 +26,11 @@ from controllers.console.wraps import ( setup_required, ) from core.ops.ops_trace_manager import OpsTraceManager +from core.rag.entities import PreProcessingRule, Rule, Segmentation from core.rag.retrieval.retrieval_methods import RetrievalMethod from core.trigger.constants import TRIGGER_NODE_TYPES from extensions.ext_database import db +from fields.base import ResponseModel from libs.login import current_account_with_tenant, login_required from models import App, DatasetPermissionEnum, Workflow from models.model import IconType @@ -41,10 +43,7 @@ from services.entities.knowledge_entities.knowledge_entities import ( NotionIcon, NotionInfo, NotionPage, - PreProcessingRule, RerankingModel, - Rule, - Segmentation, WebsiteInfo, WeightKeywordSetting, WeightModel, @@ -155,16 +154,6 @@ class AppTracePayload(BaseModel): type JSONValue = Any -class ResponseModel(BaseModel): - model_config = ConfigDict( - from_attributes=True, - extra="ignore", - populate_by_name=True, - serialize_by_alias=True, - protected_namespaces=(), - ) - - def _to_timestamp(value: datetime | int | None) -> int | None: if isinstance(value, datetime): return int(value.timestamp()) diff --git a/api/controllers/console/app/workflow_draft_variable.py b/api/controllers/console/app/workflow_draft_variable.py index 366f145360..f6d076320c 100644 --- a/api/controllers/console/app/workflow_draft_variable.py +++ b/api/controllers/console/app/workflow_draft_variable.py @@ -193,7 +193,7 @@ workflow_draft_variable_list_model = console_ns.model( ) -def _api_prerequisite(f: Callable[..., Any]) -> Callable[..., Any]: +def _api_prerequisite[**P, R](f: Callable[P, R]) -> Callable[P, R | Response]: """Common prerequisites for all draft workflow variable APIs. It ensures the following conditions are satisfied: @@ -210,7 +210,7 @@ def _api_prerequisite(f: Callable[..., Any]) -> Callable[..., Any]: @edit_permission_required @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @wraps(f) - def wrapper(*args: Any, **kwargs: Any): + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | Response: return f(*args, **kwargs) return wrapper diff --git a/api/controllers/console/app/workflow_trigger.py b/api/controllers/console/app/workflow_trigger.py index aa37d24738..e4a6afae1e 100644 --- a/api/controllers/console/app/workflow_trigger.py +++ b/api/controllers/console/app/workflow_trigger.py @@ -66,13 +66,13 @@ class WebhookTriggerApi(Resource): with sessionmaker(db.engine).begin() as session: # Get webhook trigger for this app and node - webhook_trigger = ( - session.query(WorkflowWebhookTrigger) + webhook_trigger = session.scalar( + select(WorkflowWebhookTrigger) .where( WorkflowWebhookTrigger.app_id == app_model.id, WorkflowWebhookTrigger.node_id == node_id, ) - .first() + .limit(1) ) if not webhook_trigger: diff --git a/api/controllers/console/app/wraps.py b/api/controllers/console/app/wraps.py index bd6f019eac..c9cf08072a 100644 --- a/api/controllers/console/app/wraps.py +++ b/api/controllers/console/app/wraps.py @@ -1,6 +1,6 @@ from collections.abc import Callable from functools import wraps -from typing import Any +from typing import overload from sqlalchemy import select @@ -23,14 +23,30 @@ def _load_app_model_with_trial(app_id: str) -> App | None: return app_model -def get_app_model( - view: Callable[..., Any] | None = None, +@overload +def get_app_model[**P, R]( + view: Callable[P, R], *, mode: AppMode | list[AppMode] | None = None, -) -> Callable[..., Any] | Callable[[Callable[..., Any]], Callable[..., Any]]: - def decorator(view_func: Callable[..., Any]) -> Callable[..., Any]: +) -> Callable[P, R]: ... + + +@overload +def get_app_model[**P, R]( + view: None = None, + *, + mode: AppMode | list[AppMode] | None = None, +) -> Callable[[Callable[P, R]], Callable[P, R]]: ... + + +def get_app_model[**P, R]( + view: Callable[P, R] | None = None, + *, + mode: AppMode | list[AppMode] | None = None, +) -> Callable[P, R] | Callable[[Callable[P, R]], Callable[P, R]]: + def decorator(view_func: Callable[P, R]) -> Callable[P, R]: @wraps(view_func) - def decorated_view(*args: Any, **kwargs: Any): + def decorated_view(*args: P.args, **kwargs: P.kwargs) -> R: if not kwargs.get("app_id"): raise ValueError("missing app_id in path parameters") @@ -68,14 +84,30 @@ def get_app_model( return decorator(view) -def get_app_model_with_trial( - view: Callable[..., Any] | None = None, +@overload +def get_app_model_with_trial[**P, R]( + view: Callable[P, R], *, mode: AppMode | list[AppMode] | None = None, -) -> Callable[..., Any] | Callable[[Callable[..., Any]], Callable[..., Any]]: - def decorator(view_func: Callable[..., Any]) -> Callable[..., Any]: +) -> Callable[P, R]: ... + + +@overload +def get_app_model_with_trial[**P, R]( + view: None = None, + *, + mode: AppMode | list[AppMode] | None = None, +) -> Callable[[Callable[P, R]], Callable[P, R]]: ... + + +def get_app_model_with_trial[**P, R]( + view: Callable[P, R] | None = None, + *, + mode: AppMode | list[AppMode] | None = None, +) -> Callable[P, R] | Callable[[Callable[P, R]], Callable[P, R]]: + def decorator(view_func: Callable[P, R]) -> Callable[P, R]: @wraps(view_func) - def decorated_view(*args: Any, **kwargs: Any): + def decorated_view(*args: P.args, **kwargs: P.kwargs) -> R: if not kwargs.get("app_id"): raise ValueError("missing app_id in path parameters") diff --git a/api/controllers/console/auth/forgot_password.py b/api/controllers/console/auth/forgot_password.py index 844f3c91ff..63bc98b53f 100644 --- a/api/controllers/console/auth/forgot_password.py +++ b/api/controllers/console/auth/forgot_password.py @@ -3,7 +3,7 @@ import secrets from flask import request from flask_restx import Resource -from pydantic import BaseModel, Field, field_validator +from pydantic import BaseModel, Field from sqlalchemy.orm import sessionmaker from controllers.common.schema import register_schema_models @@ -20,35 +20,18 @@ from controllers.console.wraps import email_password_login_enabled, setup_requir from events.tenant_event import tenant_was_created from extensions.ext_database import db from libs.helper import EmailStr, extract_remote_ip -from libs.password import hash_password, valid_password +from libs.password import hash_password from services.account_service import AccountService, TenantService +from services.entities.auth_entities import ( + ForgotPasswordCheckPayload, + ForgotPasswordResetPayload, + ForgotPasswordSendPayload, +) from services.feature_service import FeatureService DEFAULT_REF_TEMPLATE_SWAGGER_2_0 = "#/definitions/{model}" -class ForgotPasswordSendPayload(BaseModel): - email: EmailStr = Field(...) - language: str | None = Field(default=None) - - -class ForgotPasswordCheckPayload(BaseModel): - email: EmailStr = Field(...) - code: str = Field(...) - token: str = Field(...) - - -class ForgotPasswordResetPayload(BaseModel): - token: str = Field(...) - new_password: str = Field(...) - password_confirm: str = Field(...) - - @field_validator("new_password", "password_confirm") - @classmethod - def validate_password(cls, value: str) -> str: - return valid_password(value) - - class ForgotPasswordEmailResponse(BaseModel): result: str = Field(description="Operation result") data: str | None = Field(default=None, description="Reset token") diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 400df138b8..962cc83b0e 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -1,5 +1,3 @@ -from typing import Any - import flask_login from flask import make_response, request from flask_restx import Resource @@ -42,8 +40,9 @@ from libs.token import ( set_csrf_token_to_cookie, set_refresh_token_to_cookie, ) -from services.account_service import AccountService, RegisterService, TenantService +from services.account_service import AccountService, InvitationDetailDict, RegisterService, TenantService from services.billing_service import BillingService +from services.entities.auth_entities import LoginPayloadBase from services.errors.account import AccountRegisterError from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkspacesLimitExceededError from services.feature_service import FeatureService @@ -51,9 +50,7 @@ from services.feature_service import FeatureService DEFAULT_REF_TEMPLATE_SWAGGER_2_0 = "#/definitions/{model}" -class LoginPayload(BaseModel): - email: EmailStr = Field(..., description="Email address") - password: str = Field(..., description="Password") +class LoginPayload(LoginPayloadBase): remember_me: bool = Field(default=False, description="Remember me flag") invite_token: str | None = Field(default=None, description="Invitation token") @@ -101,7 +98,7 @@ class LoginApi(Resource): raise EmailPasswordLoginLimitError() invite_token = args.invite_token - invitation_data: dict[str, Any] | None = None + invitation_data: InvitationDetailDict | None = None if invite_token: invitation_data = RegisterService.get_invitation_with_case_fallback(None, request_email, invite_token) if invitation_data is None: diff --git a/api/controllers/console/datasets/data_source.py b/api/controllers/console/datasets/data_source.py index ac14349045..e623722b23 100644 --- a/api/controllers/console/datasets/data_source.py +++ b/api/controllers/console/datasets/data_source.py @@ -158,10 +158,11 @@ class DataSourceApi(Resource): @login_required @account_initialization_required def patch(self, binding_id, action: Literal["enable", "disable"]): + _, current_tenant_id = current_account_with_tenant() binding_id = str(binding_id) with sessionmaker(db.engine, expire_on_commit=False).begin() as session: data_source_binding = session.execute( - select(DataSourceOauthBinding).filter_by(id=binding_id) + select(DataSourceOauthBinding).filter_by(id=binding_id, tenant_id=current_tenant_id) ).scalar_one_or_none() if data_source_binding is None: raise NotFound("Data source binding not found.") diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline.py index 1758bad31d..4fe9690257 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline.py @@ -3,6 +3,7 @@ import logging from flask import request from flask_restx import Resource from pydantic import BaseModel, Field +from sqlalchemy import select from sqlalchemy.orm import sessionmaker from controllers.common.schema import register_schema_models @@ -86,8 +87,8 @@ class CustomizedPipelineTemplateApi(Resource): @enterprise_license_required def post(self, template_id: str): with sessionmaker(db.engine, expire_on_commit=False).begin() as session: - template = ( - session.query(PipelineCustomizedTemplate).where(PipelineCustomizedTemplate.id == template_id).first() + template = session.scalar( + select(PipelineCustomizedTemplate).where(PipelineCustomizedTemplate.id == template_id).limit(1) ) if not template: raise ValueError("Customized pipeline template not found.") diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py index d635dcb530..93feec0019 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py @@ -1,4 +1,5 @@ import logging +from collections.abc import Callable from typing import Any, NoReturn from flask import Response, request @@ -55,7 +56,7 @@ class WorkflowDraftVariablePatchPayload(BaseModel): register_schema_models(console_ns, WorkflowDraftVariablePatchPayload) -def _api_prerequisite(f): +def _api_prerequisite[**P, R](f: Callable[P, R]) -> Callable[P, R | Response]: """Common prerequisites for all draft workflow variable APIs. It ensures the following conditions are satisfied: @@ -70,7 +71,7 @@ def _api_prerequisite(f): @login_required @account_initialization_required @get_rag_pipeline - def wrapper(*args, **kwargs): + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | Response: if not isinstance(current_user, Account) or not current_user.has_edit_permission: raise Forbidden() return f(*args, **kwargs) diff --git a/api/controllers/console/explore/audio.py b/api/controllers/console/explore/audio.py index b1b01b5f51..a37077af42 100644 --- a/api/controllers/console/explore/audio.py +++ b/api/controllers/console/explore/audio.py @@ -2,10 +2,10 @@ import logging from flask import request from graphon.model_runtime.errors.invoke import InvokeError -from pydantic import BaseModel, Field from werkzeug.exceptions import InternalServerError import services +from controllers.common.controller_schemas import TextToAudioPayload from controllers.common.schema import register_schema_model from controllers.console.app.error import ( AppUnavailableError, @@ -32,14 +32,6 @@ from .. import console_ns logger = logging.getLogger(__name__) - -class TextToAudioPayload(BaseModel): - message_id: str | None = None - voice: str | None = None - text: str | None = None - streaming: bool | None = Field(default=None, description="Enable streaming response") - - register_schema_model(console_ns, TextToAudioPayload) diff --git a/api/controllers/console/explore/conversation.py b/api/controllers/console/explore/conversation.py index 092f509f1c..2eb2054e64 100644 --- a/api/controllers/console/explore/conversation.py +++ b/api/controllers/console/explore/conversation.py @@ -1,10 +1,11 @@ from typing import Any from flask import request -from pydantic import BaseModel, Field, TypeAdapter, model_validator +from pydantic import BaseModel, Field, TypeAdapter from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import NotFound +from controllers.common.controller_schemas import ConversationRenamePayload from controllers.common.schema import register_schema_models from controllers.console.explore.error import NotChatAppError from controllers.console.explore.wraps import InstalledAppResource @@ -32,18 +33,6 @@ class ConversationListQuery(BaseModel): pinned: bool | None = None -class ConversationRenamePayload(BaseModel): - name: str | None = None - auto_generate: bool = False - - @model_validator(mode="after") - def validate_name_requirement(self): - if not self.auto_generate: - if self.name is None or not self.name.strip(): - raise ValueError("name is required when auto_generate is false") - return self - - register_schema_models(console_ns, ConversationListQuery, ConversationRenamePayload) diff --git a/api/controllers/console/explore/message.py b/api/controllers/console/explore/message.py index fcbefcda33..64d55d7ca3 100644 --- a/api/controllers/console/explore/message.py +++ b/api/controllers/console/explore/message.py @@ -3,9 +3,10 @@ from typing import Literal from flask import request from graphon.model_runtime.errors.invoke import InvokeError -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import BaseModel, TypeAdapter from werkzeug.exceptions import InternalServerError, NotFound +from controllers.common.controller_schemas import MessageFeedbackPayload, MessageListQuery from controllers.common.schema import register_schema_models from controllers.console.app.error import ( AppMoreLikeThisDisabledError, @@ -25,7 +26,6 @@ from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotIni from fields.conversation_fields import ResultResponse from fields.message_fields import MessageInfiniteScrollPagination, MessageListItem, SuggestedQuestionsResponse from libs import helper -from libs.helper import UUIDStrOrEmpty from libs.login import current_account_with_tenant from models.enums import FeedbackRating from models.model import AppMode @@ -44,17 +44,6 @@ from .. import console_ns logger = logging.getLogger(__name__) -class MessageListQuery(BaseModel): - conversation_id: UUIDStrOrEmpty - first_id: UUIDStrOrEmpty | None = None - limit: int = Field(default=20, ge=1, le=100) - - -class MessageFeedbackPayload(BaseModel): - rating: Literal["like", "dislike"] | None = None - content: str | None = None - - class MoreLikeThisQuery(BaseModel): response_mode: Literal["blocking", "streaming"] diff --git a/api/controllers/console/explore/saved_message.py b/api/controllers/console/explore/saved_message.py index ea3de91741..9ec4e82324 100644 --- a/api/controllers/console/explore/saved_message.py +++ b/api/controllers/console/explore/saved_message.py @@ -1,28 +1,18 @@ from flask import request -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import TypeAdapter from werkzeug.exceptions import NotFound +from controllers.common.controller_schemas import SavedMessageCreatePayload, SavedMessageListQuery from controllers.common.schema import register_schema_models from controllers.console import console_ns from controllers.console.explore.error import NotCompletionAppError from controllers.console.explore.wraps import InstalledAppResource from fields.conversation_fields import ResultResponse from fields.message_fields import SavedMessageInfiniteScrollPagination, SavedMessageItem -from libs.helper import UUIDStrOrEmpty from libs.login import current_account_with_tenant from services.errors.message import MessageNotExistsError from services.saved_message_service import SavedMessageService - -class SavedMessageListQuery(BaseModel): - last_id: UUIDStrOrEmpty | None = None - limit: int = Field(default=20, ge=1, le=100) - - -class SavedMessageCreatePayload(BaseModel): - message_id: UUIDStrOrEmpty - - register_schema_models(console_ns, SavedMessageListQuery, SavedMessageCreatePayload) diff --git a/api/controllers/console/explore/workflow.py b/api/controllers/console/explore/workflow.py index 42cafc7193..da88de6776 100644 --- a/api/controllers/console/explore/workflow.py +++ b/api/controllers/console/explore/workflow.py @@ -1,11 +1,10 @@ import logging -from typing import Any from graphon.graph_engine.manager import GraphEngineManager from graphon.model_runtime.errors.invoke import InvokeError -from pydantic import BaseModel from werkzeug.exceptions import InternalServerError +from controllers.common.controller_schemas import WorkflowRunPayload from controllers.common.schema import register_schema_model from controllers.console.app.error import ( CompletionRequestError, @@ -34,12 +33,6 @@ from .. import console_ns logger = logging.getLogger(__name__) - -class WorkflowRunPayload(BaseModel): - inputs: dict[str, Any] - files: list[dict[str, Any]] | None = None - - register_schema_model(console_ns, WorkflowRunPayload) diff --git a/api/controllers/console/notification.py b/api/controllers/console/notification.py index 53e4aa3d86..180167402a 100644 --- a/api/controllers/console/notification.py +++ b/api/controllers/console/notification.py @@ -1,3 +1,5 @@ +from typing import TypedDict + from flask import request from flask_restx import Resource from pydantic import BaseModel, Field @@ -11,6 +13,21 @@ from services.billing_service import BillingService _FALLBACK_LANG = "en-US" +class NotificationItemDict(TypedDict): + notification_id: str | None + frequency: str | None + lang: str + title: str + subtitle: str + body: str + title_pic_url: str + + +class NotificationResponseDict(TypedDict): + should_show: bool + notifications: list[NotificationItemDict] + + def _pick_lang_content(contents: dict, lang: str) -> dict: """Return the single LangContent for *lang*, falling back to English.""" return contents.get(lang) or contents.get(_FALLBACK_LANG) or next(iter(contents.values()), {}) @@ -45,28 +62,30 @@ class NotificationApi(Resource): result = BillingService.get_account_notification(str(current_user.id)) # Proto JSON uses camelCase field names (Kratos default marshaling). + response: NotificationResponseDict if not result.get("shouldShow"): - return {"should_show": False, "notifications": []}, 200 + response = {"should_show": False, "notifications": []} + return response, 200 lang = current_user.interface_language or _FALLBACK_LANG - notifications = [] + notifications: list[NotificationItemDict] = [] for notification in result.get("notifications") or []: contents: dict = notification.get("contents") or {} lang_content = _pick_lang_content(contents, lang) - notifications.append( - { - "notification_id": notification.get("notificationId"), - "frequency": notification.get("frequency"), - "lang": lang_content.get("lang", lang), - "title": lang_content.get("title", ""), - "subtitle": lang_content.get("subtitle", ""), - "body": lang_content.get("body", ""), - "title_pic_url": lang_content.get("titlePicUrl", ""), - } - ) + item: NotificationItemDict = { + "notification_id": notification.get("notificationId"), + "frequency": notification.get("frequency"), + "lang": lang_content.get("lang", lang), + "title": lang_content.get("title", ""), + "subtitle": lang_content.get("subtitle", ""), + "body": lang_content.get("body", ""), + "title_pic_url": lang_content.get("titlePicUrl", ""), + } + notifications.append(item) - return {"should_show": bool(notifications), "notifications": notifications}, 200 + response = {"should_show": bool(notifications), "notifications": notifications} + return response, 200 @console_ns.route("/notification/dismiss") diff --git a/api/controllers/console/tag/tags.py b/api/controllers/console/tag/tags.py index 7511c970a3..39b84d3869 100644 --- a/api/controllers/console/tag/tags.py +++ b/api/controllers/console/tag/tags.py @@ -9,7 +9,14 @@ from controllers.common.schema import register_schema_models from controllers.console import console_ns from controllers.console.wraps import account_initialization_required, edit_permission_required, setup_required from libs.login import current_account_with_tenant, login_required -from services.tag_service import TagService +from models.enums import TagType +from services.tag_service import ( + SaveTagPayload, + TagBindingCreatePayload, + TagBindingDeletePayload, + TagService, + UpdateTagPayload, +) dataset_tag_fields = { "id": fields.String, @@ -25,19 +32,19 @@ def build_dataset_tag_fields(api_or_ns: Namespace): class TagBasePayload(BaseModel): name: str = Field(description="Tag name", min_length=1, max_length=50) - type: Literal["knowledge", "app"] | None = Field(default=None, description="Tag type") + type: TagType = Field(description="Tag type") class TagBindingPayload(BaseModel): tag_ids: list[str] = Field(description="Tag IDs to bind") target_id: str = Field(description="Target ID to bind tags to") - type: Literal["knowledge", "app"] | None = Field(default=None, description="Tag type") + type: TagType = Field(description="Tag type") class TagBindingRemovePayload(BaseModel): tag_id: str = Field(description="Tag ID to remove") target_id: str = Field(description="Target ID to unbind tag from") - type: Literal["knowledge", "app"] | None = Field(default=None, description="Tag type") + type: TagType = Field(description="Tag type") class TagListQueryParam(BaseModel): @@ -82,7 +89,7 @@ class TagListApi(Resource): raise Forbidden() payload = TagBasePayload.model_validate(console_ns.payload or {}) - tag = TagService.save_tags(payload.model_dump()) + tag = TagService.save_tags(SaveTagPayload(name=payload.name, type=payload.type)) response = {"id": tag.id, "name": tag.name, "type": tag.type, "binding_count": 0} @@ -103,7 +110,7 @@ class TagUpdateDeleteApi(Resource): raise Forbidden() payload = TagBasePayload.model_validate(console_ns.payload or {}) - tag = TagService.update_tags(payload.model_dump(), tag_id) + tag = TagService.update_tags(UpdateTagPayload(name=payload.name, type=payload.type), tag_id) binding_count = TagService.get_tag_binding_count(tag_id) @@ -136,7 +143,9 @@ class TagBindingCreateApi(Resource): raise Forbidden() payload = TagBindingPayload.model_validate(console_ns.payload or {}) - TagService.save_tag_binding(payload.model_dump()) + TagService.save_tag_binding( + TagBindingCreatePayload(tag_ids=payload.tag_ids, target_id=payload.target_id, type=payload.type) + ) return {"result": "success"}, 200 @@ -154,6 +163,8 @@ class TagBindingDeleteApi(Resource): raise Forbidden() payload = TagBindingRemovePayload.model_validate(console_ns.payload or {}) - TagService.delete_tag_binding(payload.model_dump()) + TagService.delete_tag_binding( + TagBindingDeletePayload(tag_id=payload.tag_id, target_id=payload.target_id, type=payload.type) + ) return {"result": "success"}, 200 diff --git a/api/controllers/console/workspace/__init__.py b/api/controllers/console/workspace/__init__.py index 971674cee2..60f712e476 100644 --- a/api/controllers/console/workspace/__init__.py +++ b/api/controllers/console/workspace/__init__.py @@ -1,6 +1,7 @@ from collections.abc import Callable from functools import wraps +from sqlalchemy import select from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import Forbidden @@ -21,12 +22,12 @@ def plugin_permission_required( tenant_id = current_tenant_id with sessionmaker(db.engine).begin() as session: - permission = ( - session.query(TenantPluginPermission) + permission = session.scalar( + select(TenantPluginPermission) .where( TenantPluginPermission.tenant_id == tenant_id, ) - .first() + .limit(1) ) if not permission: diff --git a/api/controllers/console/workspace/workspace.py b/api/controllers/console/workspace/workspace.py index a06b4fd195..42874e6033 100644 --- a/api/controllers/console/workspace/workspace.py +++ b/api/controllers/console/workspace/workspace.py @@ -28,7 +28,7 @@ from enums.cloud_plan import CloudPlan from extensions.ext_database import db from libs.helper import TimestampField from libs.login import current_account_with_tenant, login_required -from models.account import Tenant, TenantStatus +from models.account import Tenant, TenantCustomConfigDict, TenantStatus from services.account_service import TenantService from services.billing_service import BillingService, SubscriptionPlan from services.enterprise.enterprise_service import EnterpriseService @@ -240,8 +240,10 @@ class CustomConfigWorkspaceApi(Resource): args = WorkspaceCustomConfigPayload.model_validate(payload) tenant = db.get_or_404(Tenant, current_tenant_id) - custom_config_dict = { - "remove_webapp_brand": args.remove_webapp_brand, + custom_config_dict: TenantCustomConfigDict = { + "remove_webapp_brand": args.remove_webapp_brand + if args.remove_webapp_brand is not None + else tenant.custom_config_dict.get("remove_webapp_brand", False), "replace_webapp_logo": args.replace_webapp_logo if args.replace_webapp_logo is not None else tenant.custom_config_dict.get("replace_webapp_logo"), diff --git a/api/controllers/inner_api/app/dsl.py b/api/controllers/inner_api/app/dsl.py index 3b673d6e1d..b1986b2557 100644 --- a/api/controllers/inner_api/app/dsl.py +++ b/api/controllers/inner_api/app/dsl.py @@ -9,7 +9,7 @@ from flask import request from flask_restx import Resource from pydantic import BaseModel, Field from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from controllers.common.schema import register_schema_model from controllers.console.wraps import setup_required @@ -55,7 +55,7 @@ class EnterpriseAppDSLImport(Resource): account.set_tenant_id(workspace_id) - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: dsl_service = AppDslService(session) result = dsl_service.import_app( account=account, @@ -64,7 +64,6 @@ class EnterpriseAppDSLImport(Resource): name=args.name, description=args.description, ) - session.commit() if result.status == ImportStatus.FAILED: return result.model_dump(mode="json"), 400 diff --git a/api/controllers/mcp/mcp.py b/api/controllers/mcp/mcp.py index 3c59535a48..d2ce0ea543 100644 --- a/api/controllers/mcp/mcp.py +++ b/api/controllers/mcp/mcp.py @@ -4,6 +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 import select from sqlalchemy.orm import Session, sessionmaker from controllers.common.schema import register_schema_model @@ -80,11 +81,11 @@ class MCPAppApi(Resource): def _get_mcp_server_and_app(self, server_code: str, session: Session) -> tuple[AppMCPServer, App]: """Get and validate MCP server and app in one query session""" - mcp_server = session.query(AppMCPServer).where(AppMCPServer.server_code == server_code).first() + mcp_server = session.scalar(select(AppMCPServer).where(AppMCPServer.server_code == server_code).limit(1)) if not mcp_server: raise MCPRequestError(mcp_types.INVALID_REQUEST, "Server Not Found") - app = session.query(App).where(App.id == mcp_server.app_id).first() + app = session.scalar(select(App).where(App.id == mcp_server.app_id).limit(1)) if not app: raise MCPRequestError(mcp_types.INVALID_REQUEST, "App Not Found") @@ -190,12 +191,12 @@ 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 sessionmaker(db.engine, expire_on_commit=False).begin() as session: - return ( - session.query(EndUser) + return session.scalar( + select(EndUser) .where(EndUser.tenant_id == tenant_id) .where(EndUser.session_id == mcp_server_id) .where(EndUser.type == "mcp") - .first() + .limit(1) ) def _create_end_user( diff --git a/api/controllers/service_api/app/conversation.py b/api/controllers/service_api/app/conversation.py index 8c9a3eb5e9..1ec289e2a2 100644 --- a/api/controllers/service_api/app/conversation.py +++ b/api/controllers/service_api/app/conversation.py @@ -2,11 +2,12 @@ from typing import Any, Literal from flask import request from flask_restx import Resource -from pydantic import BaseModel, Field, TypeAdapter, field_validator, model_validator +from pydantic import BaseModel, Field, TypeAdapter, field_validator from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, NotFound import services +from controllers.common.controller_schemas import ConversationRenamePayload from controllers.common.schema import register_schema_models from controllers.service_api import service_api_ns from controllers.service_api.app.error import NotChatAppError @@ -34,18 +35,6 @@ class ConversationListQuery(BaseModel): ) -class ConversationRenamePayload(BaseModel): - name: str | None = Field(default=None, description="New conversation name (required if auto_generate is false)") - auto_generate: bool = Field(default=False, description="Auto-generate conversation name") - - @model_validator(mode="after") - def validate_name_requirement(self): - if not self.auto_generate: - if self.name is None or not self.name.strip(): - raise ValueError("name is required when auto_generate is false") - return self - - class ConversationVariablesQuery(BaseModel): last_id: UUIDStrOrEmpty | None = Field(default=None, description="Last variable ID for pagination") limit: int = Field(default=20, ge=1, le=100, description="Number of variables to return") diff --git a/api/controllers/service_api/app/message.py b/api/controllers/service_api/app/message.py index 77fee9c142..b75b299f6f 100644 --- a/api/controllers/service_api/app/message.py +++ b/api/controllers/service_api/app/message.py @@ -1,5 +1,4 @@ import logging -from typing import Literal from flask import request from flask_restx import Resource @@ -7,6 +6,7 @@ from pydantic import BaseModel, Field, TypeAdapter from werkzeug.exceptions import BadRequest, InternalServerError, NotFound import services +from controllers.common.controller_schemas import MessageFeedbackPayload, MessageListQuery from controllers.common.schema import register_schema_models from controllers.service_api import service_api_ns from controllers.service_api.app.error import NotChatAppError @@ -14,7 +14,6 @@ from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate from core.app.entities.app_invoke_entities import InvokeFrom from fields.conversation_fields import ResultResponse from fields.message_fields import MessageInfiniteScrollPagination, MessageListItem -from libs.helper import UUIDStrOrEmpty from models.enums import FeedbackRating from models.model import App, AppMode, EndUser from services.errors.message import ( @@ -27,17 +26,6 @@ from services.message_service import MessageService logger = logging.getLogger(__name__) -class MessageListQuery(BaseModel): - conversation_id: UUIDStrOrEmpty - first_id: UUIDStrOrEmpty | None = None - limit: int = Field(default=20, ge=1, le=100, description="Number of messages to return") - - -class MessageFeedbackPayload(BaseModel): - rating: Literal["like", "dislike"] | None = Field(default=None, description="Feedback rating") - content: str | None = Field(default=None, description="Feedback content") - - class FeedbackListQuery(BaseModel): page: int = Field(default=1, ge=1, description="Page number") limit: int = Field(default=20, ge=1, le=101, description="Number of feedbacks per page") diff --git a/api/controllers/service_api/app/workflow.py b/api/controllers/service_api/app/workflow.py index d7992a2a3a..e0a64ffe26 100644 --- a/api/controllers/service_api/app/workflow.py +++ b/api/controllers/service_api/app/workflow.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Literal +from typing import Literal from dateutil.parser import isoparse from flask import request @@ -11,6 +11,7 @@ from pydantic import BaseModel, Field from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, InternalServerError, NotFound +from controllers.common.controller_schemas import WorkflowRunPayload as WorkflowRunPayloadBase from controllers.common.schema import register_schema_models from controllers.service_api import service_api_ns from controllers.service_api.app.error import ( @@ -46,9 +47,7 @@ from services.workflow_app_service import WorkflowAppService logger = logging.getLogger(__name__) -class WorkflowRunPayload(BaseModel): - inputs: dict[str, Any] - files: list[dict[str, Any]] | None = None +class WorkflowRunPayload(WorkflowRunPayloadBase): response_mode: Literal["blocking", "streaming"] | None = None diff --git a/api/controllers/service_api/dataset/dataset.py b/api/controllers/service_api/dataset/dataset.py index 80205b283b..fd954be6b1 100644 --- a/api/controllers/service_api/dataset/dataset.py +++ b/api/controllers/service_api/dataset/dataset.py @@ -22,10 +22,17 @@ from fields.tag_fields import DataSetTag from libs.login import current_user from models.account import Account from models.dataset import DatasetPermissionEnum +from models.enums import TagType from models.provider_ids import ModelProviderID from services.dataset_service import DatasetPermissionService, DatasetService, DocumentService from services.entities.knowledge_entities.knowledge_entities import RetrievalModel -from services.tag_service import TagService +from services.tag_service import ( + SaveTagPayload, + TagBindingCreatePayload, + TagBindingDeletePayload, + TagService, + UpdateTagPayload, +) DEFAULT_REF_TEMPLATE_SWAGGER_2_0 = "#/definitions/{model}" @@ -513,7 +520,7 @@ class DatasetTagsApi(DatasetApiResource): raise Forbidden() payload = TagCreatePayload.model_validate(service_api_ns.payload or {}) - tag = TagService.save_tags({"name": payload.name, "type": "knowledge"}) + tag = TagService.save_tags(SaveTagPayload(name=payload.name, type=TagType.KNOWLEDGE)) response = DataSetTag.model_validate( {"id": tag.id, "name": tag.name, "type": tag.type, "binding_count": 0} @@ -536,9 +543,8 @@ class DatasetTagsApi(DatasetApiResource): raise Forbidden() payload = TagUpdatePayload.model_validate(service_api_ns.payload or {}) - params = {"name": payload.name, "type": "knowledge"} tag_id = payload.tag_id - tag = TagService.update_tags(params, tag_id) + tag = TagService.update_tags(UpdateTagPayload(name=payload.name, type=TagType.KNOWLEDGE), tag_id) binding_count = TagService.get_tag_binding_count(tag_id) @@ -585,7 +591,9 @@ class DatasetTagBindingApi(DatasetApiResource): raise Forbidden() payload = TagBindingPayload.model_validate(service_api_ns.payload or {}) - TagService.save_tag_binding({"tag_ids": payload.tag_ids, "target_id": payload.target_id, "type": "knowledge"}) + TagService.save_tag_binding( + TagBindingCreatePayload(tag_ids=payload.tag_ids, target_id=payload.target_id, type=TagType.KNOWLEDGE) + ) return "", 204 @@ -609,7 +617,9 @@ class DatasetTagUnbindingApi(DatasetApiResource): raise Forbidden() payload = TagUnbindingPayload.model_validate(service_api_ns.payload or {}) - TagService.delete_tag_binding({"tag_id": payload.tag_id, "target_id": payload.target_id, "type": "knowledge"}) + TagService.delete_tag_binding( + TagBindingDeletePayload(tag_id=payload.tag_id, target_id=payload.target_id, type=TagType.KNOWLEDGE) + ) return "", 204 diff --git a/api/controllers/service_api/dataset/document.py b/api/controllers/service_api/dataset/document.py index 2c094aa3e6..9f1ce17ed9 100644 --- a/api/controllers/service_api/dataset/document.py +++ b/api/controllers/service_api/dataset/document.py @@ -31,6 +31,7 @@ from controllers.service_api.wraps import ( cloud_edition_billing_resource_check, ) from core.errors.error import ProviderTokenNotInitError +from core.rag.entities import PreProcessingRule, Rule, Segmentation from core.rag.retrieval.retrieval_methods import RetrievalMethod from extensions.ext_database import db from fields.document_fields import document_fields, document_status_fields @@ -40,11 +41,8 @@ from models.enums import SegmentStatus from services.dataset_service import DatasetService, DocumentService from services.entities.knowledge_entities.knowledge_entities import ( KnowledgeConfig, - PreProcessingRule, ProcessRule, RetrievalModel, - Rule, - Segmentation, ) from services.file_service import FileService from services.summary_index_service import SummaryIndexService diff --git a/api/controllers/service_api/dataset/rag_pipeline/serializers.py b/api/controllers/service_api/dataset/rag_pipeline/serializers.py index 8533c9c01d..a5e8484037 100644 --- a/api/controllers/service_api/dataset/rag_pipeline/serializers.py +++ b/api/controllers/service_api/dataset/rag_pipeline/serializers.py @@ -4,13 +4,23 @@ Serialization helpers for Service API knowledge pipeline endpoints. from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, TypedDict if TYPE_CHECKING: from models.model import UploadFile -def serialize_upload_file(upload_file: UploadFile) -> dict[str, Any]: +class UploadFileDict(TypedDict): + id: str + name: str + size: int + extension: str + mime_type: str | None + created_by: str + created_at: str | None + + +def serialize_upload_file(upload_file: UploadFile) -> UploadFileDict: return { "id": upload_file.id, "name": upload_file.name, diff --git a/api/controllers/service_api/wraps.py b/api/controllers/service_api/wraps.py index 2dd916bb31..b9389ccc47 100644 --- a/api/controllers/service_api/wraps.py +++ b/api/controllers/service_api/wraps.py @@ -1,9 +1,10 @@ +import inspect import logging import time from collections.abc import Callable from enum import StrEnum, auto from functools import wraps -from typing import Any, cast, overload +from typing import cast, overload from flask import current_app, request from flask_login import user_logged_in @@ -230,94 +231,73 @@ def cloud_edition_billing_rate_limit_check[**P, R]( return interceptor -def validate_dataset_token( - view: Callable[..., Any] | None = None, -) -> Callable[..., Any] | Callable[[Callable[..., Any]], Callable[..., Any]]: - def decorator(view_func: Callable[..., Any]) -> Callable[..., Any]: - @wraps(view_func) - def decorated(*args: Any, **kwargs: Any) -> Any: - api_token = validate_and_get_api_token("dataset") +def validate_dataset_token[R](view: Callable[..., R]) -> Callable[..., R]: + positional_parameters = [ + parameter + for parameter in inspect.signature(view).parameters.values() + if parameter.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD) + ] + expects_bound_instance = bool(positional_parameters and positional_parameters[0].name in {"self", "cls"}) - # get url path dataset_id from positional args or kwargs - # Flask passes URL path parameters as positional arguments - dataset_id = None + @wraps(view) + def decorated(*args: object, **kwargs: object) -> R: + api_token = validate_and_get_api_token("dataset") - # First try to get from kwargs (explicit parameter) - dataset_id = kwargs.get("dataset_id") + # Flask may pass URL path parameters positionally, so inspect both kwargs and args. + dataset_id = kwargs.get("dataset_id") - # If not in kwargs, try to extract from positional args - if not dataset_id and args: - # For class methods: args[0] is self, args[1] is dataset_id (if exists) - # Check if first arg is likely a class instance (has __dict__ or __class__) - if len(args) > 1 and hasattr(args[0], "__dict__"): - # This is a class method, dataset_id should be in args[1] - potential_id = args[1] - # Validate it's a string-like UUID, not another object - try: - # Try to convert to string and check if it's a valid UUID format - str_id = str(potential_id) - # Basic check: UUIDs are 36 chars with hyphens - if len(str_id) == 36 and str_id.count("-") == 4: - dataset_id = str_id - except Exception: - logger.exception("Failed to parse dataset_id from class method args") - elif len(args) > 0: - # Not a class method, check if args[0] looks like a UUID - potential_id = args[0] - try: - str_id = str(potential_id) - if len(str_id) == 36 and str_id.count("-") == 4: - dataset_id = str_id - except Exception: - logger.exception("Failed to parse dataset_id from positional args") + if not dataset_id and args: + potential_id = args[0] + try: + str_id = str(potential_id) + if len(str_id) == 36 and str_id.count("-") == 4: + dataset_id = str_id + except Exception: + logger.exception("Failed to parse dataset_id from positional args") - # Validate dataset if dataset_id is provided - if dataset_id: - dataset_id = str(dataset_id) - dataset = db.session.scalar( - select(Dataset) - .where( - Dataset.id == dataset_id, - Dataset.tenant_id == api_token.tenant_id, - ) - .limit(1) + if dataset_id: + dataset_id = str(dataset_id) + dataset = db.session.scalar( + select(Dataset) + .where( + Dataset.id == dataset_id, + Dataset.tenant_id == api_token.tenant_id, ) - if not dataset: - raise NotFound("Dataset not found.") - if not dataset.enable_api: - raise Forbidden("Dataset api access is not enabled.") - tenant_account_join = db.session.execute( - select(Tenant, TenantAccountJoin) - .where(Tenant.id == api_token.tenant_id) - .where(TenantAccountJoin.tenant_id == Tenant.id) - .where(TenantAccountJoin.role.in_(["owner"])) - .where(Tenant.status == TenantStatus.NORMAL) - ).one_or_none() # TODO: only owner information is required, so only one is returned. - if tenant_account_join: - tenant, ta = tenant_account_join - account = db.session.get(Account, ta.account_id) - # Login admin - if account: - account.current_tenant = tenant - current_app.login_manager._update_request_context_with_user(account) # type: ignore - user_logged_in.send(current_app._get_current_object(), user=current_user) # type: ignore - else: - raise Unauthorized("Tenant owner account does not exist.") + .limit(1) + ) + if not dataset: + raise NotFound("Dataset not found.") + if not dataset.enable_api: + raise Forbidden("Dataset api access is not enabled.") + + tenant_account_join = db.session.execute( + select(Tenant, TenantAccountJoin) + .where(Tenant.id == api_token.tenant_id) + .where(TenantAccountJoin.tenant_id == Tenant.id) + .where(TenantAccountJoin.role.in_(["owner"])) + .where(Tenant.status == TenantStatus.NORMAL) + ).one_or_none() # TODO: only owner information is required, so only one is returned. + if tenant_account_join: + tenant, ta = tenant_account_join + account = db.session.get(Account, ta.account_id) + # Login admin + if account: + account.current_tenant = tenant + current_app.login_manager._update_request_context_with_user(account) # type: ignore + user_logged_in.send(current_app._get_current_object(), user=current_user) # type: ignore else: - raise Unauthorized("Tenant does not exist.") - if args and isinstance(args[0], Resource): - return view_func(args[0], api_token.tenant_id, *args[1:], **kwargs) + raise Unauthorized("Tenant owner account does not exist.") + else: + raise Unauthorized("Tenant does not exist.") - return view_func(api_token.tenant_id, *args, **kwargs) + if expects_bound_instance: + if not args: + raise TypeError("validate_dataset_token expected a bound resource instance.") + return view(args[0], api_token.tenant_id, *args[1:], **kwargs) - return decorated + return view(api_token.tenant_id, *args, **kwargs) - if view: - return decorator(view) - - # if view is None, it means that the decorator is used without parentheses - # use the decorator as a function for method_decorators - return decorator + return decorated def validate_and_get_api_token(scope: str | None = None): diff --git a/api/controllers/trigger/webhook.py b/api/controllers/trigger/webhook.py index eb579da5d4..213704383c 100644 --- a/api/controllers/trigger/webhook.py +++ b/api/controllers/trigger/webhook.py @@ -7,7 +7,7 @@ from werkzeug.exceptions import NotFound, RequestEntityTooLarge from controllers.trigger import bp from core.trigger.debug.event_bus import TriggerDebugEventBus from core.trigger.debug.events import WebhookDebugEvent, build_webhook_pool_key -from services.trigger.webhook_service import WebhookService +from services.trigger.webhook_service import RawWebhookDataDict, WebhookService logger = logging.getLogger(__name__) @@ -23,6 +23,7 @@ def _prepare_webhook_execution(webhook_id: str, is_debug: bool = False): webhook_id, is_debug=is_debug ) + webhook_data: RawWebhookDataDict try: # Use new unified extraction and validation webhook_data = WebhookService.extract_and_validate_webhook_data(webhook_trigger, node_config) diff --git a/api/controllers/web/audio.py b/api/controllers/web/audio.py index 9ba1dc4a3a..0ef4471018 100644 --- a/api/controllers/web/audio.py +++ b/api/controllers/web/audio.py @@ -3,10 +3,11 @@ import logging from flask import request from flask_restx import fields, marshal_with from graphon.model_runtime.errors.invoke import InvokeError -from pydantic import BaseModel, field_validator +from pydantic import field_validator from werkzeug.exceptions import InternalServerError import services +from controllers.common.controller_schemas import TextToAudioPayload as TextToAudioPayloadBase from controllers.web import web_ns from controllers.web.error import ( AppUnavailableError, @@ -34,12 +35,7 @@ from services.errors.audio import ( from ..common.schema import register_schema_models -class TextToAudioPayload(BaseModel): - message_id: str | None = None - voice: str | None = None - text: str | None = None - streaming: bool | None = None - +class TextToAudioPayload(TextToAudioPayloadBase): @field_validator("message_id") @classmethod def validate_message_id(cls, value: str | None) -> str | None: diff --git a/api/controllers/web/conversation.py b/api/controllers/web/conversation.py index d5baa5fb7d..3975dd85c8 100644 --- a/api/controllers/web/conversation.py +++ b/api/controllers/web/conversation.py @@ -1,10 +1,11 @@ from typing import Literal from flask import request -from pydantic import BaseModel, Field, TypeAdapter, field_validator, model_validator +from pydantic import BaseModel, Field, TypeAdapter, field_validator from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import NotFound +from controllers.common.controller_schemas import ConversationRenamePayload from controllers.common.schema import register_schema_models from controllers.web import web_ns from controllers.web.error import NotChatAppError @@ -37,18 +38,6 @@ class ConversationListQuery(BaseModel): return uuid_value(value) -class ConversationRenamePayload(BaseModel): - name: str | None = None - auto_generate: bool = False - - @model_validator(mode="after") - def validate_name_requirement(self): - if not self.auto_generate: - if self.name is None or not self.name.strip(): - raise ValueError("name is required when auto_generate is false") - return self - - register_schema_models(web_ns, ConversationListQuery, ConversationRenamePayload) diff --git a/api/controllers/web/forgot_password.py b/api/controllers/web/forgot_password.py index d69571cc9c..80c3289fb4 100644 --- a/api/controllers/web/forgot_password.py +++ b/api/controllers/web/forgot_password.py @@ -3,7 +3,6 @@ import secrets from flask import request from flask_restx import Resource -from pydantic import BaseModel, Field, field_validator from sqlalchemy.orm import sessionmaker from controllers.common.schema import register_schema_models @@ -19,33 +18,15 @@ from controllers.console.error import EmailSendIpLimitError from controllers.console.wraps import email_password_login_enabled, only_edition_enterprise, setup_required from controllers.web import web_ns from extensions.ext_database import db -from libs.helper import EmailStr, extract_remote_ip -from libs.password import hash_password, valid_password +from libs.helper import extract_remote_ip +from libs.password import hash_password from models.account import Account from services.account_service import AccountService - - -class ForgotPasswordSendPayload(BaseModel): - email: EmailStr - language: str | None = None - - -class ForgotPasswordCheckPayload(BaseModel): - email: EmailStr - code: str - token: str = Field(min_length=1) - - -class ForgotPasswordResetPayload(BaseModel): - token: str = Field(min_length=1) - new_password: str - password_confirm: str - - @field_validator("new_password", "password_confirm") - @classmethod - def validate_password(cls, value: str) -> str: - return valid_password(value) - +from services.entities.auth_entities import ( + ForgotPasswordCheckPayload, + ForgotPasswordResetPayload, + ForgotPasswordSendPayload, +) register_schema_models(web_ns, ForgotPasswordSendPayload, ForgotPasswordCheckPayload, ForgotPasswordResetPayload) diff --git a/api/controllers/web/login.py b/api/controllers/web/login.py index a824f6d487..ae0e6789ef 100644 --- a/api/controllers/web/login.py +++ b/api/controllers/web/login.py @@ -29,13 +29,11 @@ from libs.token import ( ) from services.account_service import AccountService from services.app_service import AppService +from services.entities.auth_entities import LoginPayloadBase from services.webapp_auth_service import WebAppAuthService -class LoginPayload(BaseModel): - email: EmailStr - password: str - +class LoginPayload(LoginPayloadBase): @field_validator("password") @classmethod def validate_password(cls, value: str) -> str: diff --git a/api/controllers/web/message.py b/api/controllers/web/message.py index c5505dd60d..25cb6b2b9e 100644 --- a/api/controllers/web/message.py +++ b/api/controllers/web/message.py @@ -6,6 +6,7 @@ from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel, Field, TypeAdapter, field_validator from werkzeug.exceptions import InternalServerError, NotFound +from controllers.common.controller_schemas import MessageFeedbackPayload from controllers.common.schema import register_schema_models from controllers.web import web_ns from controllers.web.error import ( @@ -53,11 +54,6 @@ class MessageListQuery(BaseModel): return uuid_value(value) -class MessageFeedbackPayload(BaseModel): - rating: Literal["like", "dislike"] | None = Field(default=None, description="Feedback rating") - content: str | None = Field(default=None, description="Feedback content") - - class MessageMoreLikeThisQuery(BaseModel): response_mode: Literal["blocking", "streaming"] = Field( description="Response mode", diff --git a/api/controllers/web/saved_message.py b/api/controllers/web/saved_message.py index 29993100f6..5b206f9a98 100644 --- a/api/controllers/web/saved_message.py +++ b/api/controllers/web/saved_message.py @@ -1,27 +1,17 @@ from flask import request -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import TypeAdapter from werkzeug.exceptions import NotFound +from controllers.common.controller_schemas import SavedMessageCreatePayload, SavedMessageListQuery from controllers.common.schema import register_schema_models from controllers.web import web_ns from controllers.web.error import NotCompletionAppError from controllers.web.wraps import WebApiResource from fields.conversation_fields import ResultResponse from fields.message_fields import SavedMessageInfiniteScrollPagination, SavedMessageItem -from libs.helper import UUIDStrOrEmpty from services.errors.message import MessageNotExistsError from services.saved_message_service import SavedMessageService - -class SavedMessageListQuery(BaseModel): - last_id: UUIDStrOrEmpty | None = None - limit: int = Field(default=20, ge=1, le=100) - - -class SavedMessageCreatePayload(BaseModel): - message_id: UUIDStrOrEmpty - - register_schema_models(web_ns, SavedMessageListQuery, SavedMessageCreatePayload) diff --git a/api/controllers/web/workflow.py b/api/controllers/web/workflow.py index 7f5521f9f5..796e090976 100644 --- a/api/controllers/web/workflow.py +++ b/api/controllers/web/workflow.py @@ -1,11 +1,10 @@ import logging -from typing import Any from graphon.graph_engine.manager import GraphEngineManager from graphon.model_runtime.errors.invoke import InvokeError -from pydantic import BaseModel, Field from werkzeug.exceptions import InternalServerError +from controllers.common.controller_schemas import WorkflowRunPayload from controllers.common.schema import register_schema_models from controllers.web import web_ns from controllers.web.error import ( @@ -30,12 +29,6 @@ from models.model import App, AppMode, EndUser from services.app_generate_service import AppGenerateService from services.errors.llm import InvokeRateLimitError - -class WorkflowRunPayload(BaseModel): - inputs: dict[str, Any] = Field(description="Input variables for the workflow") - files: list[dict[str, Any]] | None = Field(default=None, description="Files to be processed by the workflow") - - logger = logging.getLogger(__name__) register_schema_models(web_ns, WorkflowRunPayload) diff --git a/api/core/agent/cot_chat_agent_runner.py b/api/core/agent/cot_chat_agent_runner.py index a4c438e929..2b2e26987e 100644 --- a/api/core/agent/cot_chat_agent_runner.py +++ b/api/core/agent/cot_chat_agent_runner.py @@ -79,21 +79,18 @@ class CotChatAgentRunner(CotAgentRunner): if not agent_scratchpad: assistant_messages = [] else: - assistant_message = AssistantPromptMessage(content="") - assistant_message.content = "" # FIXME: type check tell mypy that assistant_message.content is str + content = "" for unit in agent_scratchpad: if unit.is_final(): - assert isinstance(assistant_message.content, str) - assistant_message.content += f"Final Answer: {unit.agent_response}" + content += f"Final Answer: {unit.agent_response}" else: - assert isinstance(assistant_message.content, str) - assistant_message.content += f"Thought: {unit.thought}\n\n" + content += f"Thought: {unit.thought}\n\n" if unit.action_str: - assistant_message.content += f"Action: {unit.action_str}\n\n" + content += f"Action: {unit.action_str}\n\n" if unit.observation: - assistant_message.content += f"Observation: {unit.observation}\n\n" + content += f"Observation: {unit.observation}\n\n" - assistant_messages = [assistant_message] + assistant_messages = [AssistantPromptMessage(content=content)] # query messages query_messages = self._organize_user_query(self._query, []) diff --git a/api/core/app/app_config/common/parameters_mapping/__init__.py b/api/core/app/app_config/common/parameters_mapping/__init__.py index 460fdfb3ba..68686ceda6 100644 --- a/api/core/app/app_config/common/parameters_mapping/__init__.py +++ b/api/core/app/app_config/common/parameters_mapping/__init__.py @@ -5,6 +5,10 @@ from configs import dify_config from constants import DEFAULT_FILE_NUMBER_LIMITS +class FeatureToggleDict(TypedDict): + enabled: bool + + class SystemParametersDict(TypedDict): image_file_size_limit: int video_file_size_limit: int @@ -16,12 +20,12 @@ class SystemParametersDict(TypedDict): class AppParametersDict(TypedDict): opening_statement: str | None suggested_questions: list[str] - suggested_questions_after_answer: dict[str, Any] - speech_to_text: dict[str, Any] - text_to_speech: dict[str, Any] - retriever_resource: dict[str, Any] - annotation_reply: dict[str, Any] - more_like_this: dict[str, Any] + suggested_questions_after_answer: FeatureToggleDict + speech_to_text: FeatureToggleDict + text_to_speech: FeatureToggleDict + retriever_resource: FeatureToggleDict + annotation_reply: FeatureToggleDict + more_like_this: FeatureToggleDict user_input_form: list[dict[str, Any]] sensitive_word_avoidance: dict[str, Any] file_upload: dict[str, Any] diff --git a/api/core/app/app_config/entities.py b/api/core/app/app_config/entities.py index 536617edba..819aca864c 100644 --- a/api/core/app/app_config/entities.py +++ b/api/core/app/app_config/entities.py @@ -1,4 +1,3 @@ -from collections.abc import Sequence from enum import StrEnum, auto from typing import Any, Literal @@ -9,6 +8,7 @@ from graphon.variables.input_entities import VariableEntity as WorkflowVariableE from pydantic import BaseModel, Field from core.rag.data_post_processor.data_post_processor import RerankingModelDict, WeightsDict +from core.rag.entities import MetadataFilteringCondition from models.model import AppMode @@ -111,31 +111,6 @@ class ExternalDataVariableEntity(BaseModel): config: dict[str, Any] = Field(default_factory=dict) -SupportedComparisonOperator = Literal[ - # for string or array - "contains", - "not contains", - "start with", - "end with", - "is", - "is not", - "empty", - "not empty", - "in", - "not in", - # for number - "=", - "≠", - ">", - "<", - "≥", - "≤", - # for time - "before", - "after", -] - - class ModelConfig(BaseModel): provider: str name: str @@ -143,25 +118,6 @@ class ModelConfig(BaseModel): completion_params: dict[str, Any] = Field(default_factory=dict) -class Condition(BaseModel): - """ - Condition detail - """ - - name: str - comparison_operator: SupportedComparisonOperator - value: str | Sequence[str] | None | int | float = None - - -class MetadataFilteringCondition(BaseModel): - """ - Metadata Filtering Condition. - """ - - logical_operator: Literal["and", "or"] | None = "and" - conditions: list[Condition] | None = Field(default=None, deprecated=True) - - class DatasetRetrieveConfigEntity(BaseModel): """ Dataset Retrieve Config Entity. diff --git a/api/core/app/apps/base_app_generate_response_converter.py b/api/core/app/apps/base_app_generate_response_converter.py index 66390116d4..6e5a86505c 100644 --- a/api/core/app/apps/base_app_generate_response_converter.py +++ b/api/core/app/apps/base_app_generate_response_converter.py @@ -107,13 +107,13 @@ class AppGenerateResponseConverter(ABC): return metadata @classmethod - def _error_to_stream_response(cls, e: Exception): + def _error_to_stream_response(cls, e: Exception) -> dict[str, Any]: """ Error to stream response. :param e: exception :return: """ - error_responses = { + error_responses: dict[type[Exception], dict[str, Any]] = { ValueError: {"code": "invalid_param", "status": 400}, ProviderTokenNotInitError: {"code": "provider_not_initialize", "status": 400}, QuotaExceededError: { @@ -127,7 +127,7 @@ class AppGenerateResponseConverter(ABC): } # Determine the response based on the type of exception - data = None + data: dict[str, Any] | None = None for k, v in error_responses.items(): if isinstance(e, k): data = v diff --git a/api/core/app/apps/workflow_app_runner.py b/api/core/app/apps/workflow_app_runner.py index f68c8e60b4..caa6b82bab 100644 --- a/api/core/app/apps/workflow_app_runner.py +++ b/api/core/app/apps/workflow_app_runner.py @@ -66,7 +66,7 @@ from core.app.entities.queue_entities import ( QueueWorkflowStartedEvent, QueueWorkflowSucceededEvent, ) -from core.rag.entities.citation_metadata import RetrievalSourceMetadata +from core.rag.entities import RetrievalSourceMetadata from core.workflow.node_factory import DifyNodeFactory, get_default_root_node_id, resolve_workflow_node_class from core.workflow.system_variables import ( build_bootstrap_variables, diff --git a/api/core/app/entities/queue_entities.py b/api/core/app/entities/queue_entities.py index 5e56341f89..482f995d8e 100644 --- a/api/core/app/entities/queue_entities.py +++ b/api/core/app/entities/queue_entities.py @@ -10,7 +10,7 @@ from graphon.model_runtime.entities.llm_entities import LLMResult, LLMResultChun from pydantic import BaseModel, ConfigDict, Field from core.app.entities.agent_strategy import AgentStrategyInfo -from core.rag.entities.citation_metadata import RetrievalSourceMetadata +from core.rag.entities import RetrievalSourceMetadata class QueueEvent(StrEnum): diff --git a/api/core/app/entities/task_entities.py b/api/core/app/entities/task_entities.py index ba3b2e356f..62df85b13f 100644 --- a/api/core/app/entities/task_entities.py +++ b/api/core/app/entities/task_entities.py @@ -9,7 +9,7 @@ from graphon.nodes.human_input.entities import FormInput, UserAction from pydantic import BaseModel, ConfigDict, Field from core.app.entities.agent_strategy import AgentStrategyInfo -from core.rag.entities.citation_metadata import RetrievalSourceMetadata +from core.rag.entities import RetrievalSourceMetadata class AnnotationReplyAccount(BaseModel): diff --git a/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py b/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py index e0e6a6f5c3..9df78a7830 100644 --- a/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py +++ b/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py @@ -509,8 +509,8 @@ class EasyUIBasedGenerateTaskPipeline(BasedGenerateTaskPipeline): :return: """ with Session(db.engine, expire_on_commit=False) as session: - agent_thought: MessageAgentThought | None = ( - session.query(MessageAgentThought).where(MessageAgentThought.id == event.agent_thought_id).first() + agent_thought: MessageAgentThought | None = session.scalar( + select(MessageAgentThought).where(MessageAgentThought.id == event.agent_thought_id).limit(1) ) if agent_thought: diff --git a/api/core/callback_handler/index_tool_callback_handler.py b/api/core/callback_handler/index_tool_callback_handler.py index 6a07119244..205e004290 100644 --- a/api/core/callback_handler/index_tool_callback_handler.py +++ b/api/core/callback_handler/index_tool_callback_handler.py @@ -6,7 +6,7 @@ from sqlalchemy import select, update from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.queue_entities import QueueRetrieverResourcesEvent -from core.rag.entities.citation_metadata import RetrievalSourceMetadata +from core.rag.entities import RetrievalSourceMetadata from core.rag.index_processor.constant.index_type import IndexStructureType from core.rag.models.document import Document from extensions.ext_database import db diff --git a/api/core/datasource/datasource_manager.py b/api/core/datasource/datasource_manager.py index 143d1e696b..a5297fa33a 100644 --- a/api/core/datasource/datasource_manager.py +++ b/api/core/datasource/datasource_manager.py @@ -345,8 +345,8 @@ class DatasourceManager: @classmethod def get_upload_file_by_id(cls, file_id: str, tenant_id: str) -> File: with session_factory.create_session() as session: - upload_file = ( - session.query(UploadFile).where(UploadFile.id == file_id, UploadFile.tenant_id == tenant_id).first() + upload_file = session.scalar( + select(UploadFile).where(UploadFile.id == file_id, UploadFile.tenant_id == tenant_id).limit(1) ) if not upload_file: raise ValueError(f"UploadFile not found for file_id={file_id}, tenant_id={tenant_id}") diff --git a/api/core/datasource/entities/common_entities.py b/api/core/datasource/entities/common_entities.py index 3c64632dbb..726dafaa62 100644 --- a/api/core/datasource/entities/common_entities.py +++ b/api/core/datasource/entities/common_entities.py @@ -1,22 +1,3 @@ -from pydantic import BaseModel, Field, model_validator +from core.tools.entities.common_entities import I18nObject, I18nObjectDict - -class I18nObject(BaseModel): - """ - Model class for i18n object. - """ - - en_US: str - zh_Hans: str | None = Field(default=None) - pt_BR: str | None = Field(default=None) - ja_JP: str | None = Field(default=None) - - @model_validator(mode="after") - def _(self): - self.zh_Hans = self.zh_Hans or self.en_US - self.pt_BR = self.pt_BR or self.en_US - self.ja_JP = self.ja_JP or self.en_US - return self - - def to_dict(self) -> dict: - return {"zh_Hans": self.zh_Hans, "en_US": self.en_US, "pt_BR": self.pt_BR, "ja_JP": self.ja_JP} +__all__ = ["I18nObject", "I18nObjectDict"] diff --git a/api/core/datasource/entities/datasource_entities.py b/api/core/datasource/entities/datasource_entities.py index a063a3680b..f20bab53f0 100644 --- a/api/core/datasource/entities/datasource_entities.py +++ b/api/core/datasource/entities/datasource_entities.py @@ -9,7 +9,7 @@ from yarl import URL from configs import dify_config from core.entities.provider_entities import ProviderConfig -from core.plugin.entities.oauth import OAuthSchema +from core.plugin.entities import OAuthSchema from core.plugin.entities.parameters import ( PluginParameter, PluginParameterOption, diff --git a/api/core/entities/__init__.py b/api/core/entities/__init__.py index b848da3664..e77eac87ba 100644 --- a/api/core/entities/__init__.py +++ b/api/core/entities/__init__.py @@ -1 +1,8 @@ +from core.entities.plugin_credential_type import PluginCredentialType + DEFAULT_PLUGIN_ID = "langgenius" + +__all__ = [ + "DEFAULT_PLUGIN_ID", + "PluginCredentialType", +] diff --git a/api/core/entities/plugin_credential_type.py b/api/core/entities/plugin_credential_type.py new file mode 100644 index 0000000000..005e92473c --- /dev/null +++ b/api/core/entities/plugin_credential_type.py @@ -0,0 +1,9 @@ +import enum + + +class PluginCredentialType(enum.Enum): + MODEL = 0 # must be 0 for API contract compatibility + TOOL = 1 # must be 1 for API contract compatibility + + def to_number(self): + return self.value diff --git a/api/core/entities/provider_configuration.py b/api/core/entities/provider_configuration.py index 782897aea9..f3b2c31465 100644 --- a/api/core/entities/provider_configuration.py +++ b/api/core/entities/provider_configuration.py @@ -22,6 +22,7 @@ from sqlalchemy import func, select from sqlalchemy.orm import Session from constants import HIDDEN_VALUE +from core.entities import PluginCredentialType from core.entities.model_entities import ModelStatus, ModelWithProviderEntity, SimpleModelProviderEntity from core.entities.provider_entities import ( CustomConfiguration, @@ -46,7 +47,6 @@ from models.provider import ( TenantPreferredModelProvider, ) from models.provider_ids import ModelProviderID -from services.enterprise.plugin_manager_service import PluginCredentialType logger = logging.getLogger(__name__) diff --git a/api/core/helper/credential_utils.py b/api/core/helper/credential_utils.py index 240f498181..882639a16a 100644 --- a/api/core/helper/credential_utils.py +++ b/api/core/helper/credential_utils.py @@ -2,7 +2,7 @@ Credential utility functions for checking credential existence and policy compliance. """ -from services.enterprise.plugin_manager_service import PluginCredentialType +from core.entities import PluginCredentialType def is_credential_exists(credential_id: str, credential_type: "PluginCredentialType") -> bool: diff --git a/api/core/llm_generator/llm_generator.py b/api/core/llm_generator/llm_generator.py index d39630ad95..aa258c9f89 100644 --- a/api/core/llm_generator/llm_generator.py +++ b/api/core/llm_generator/llm_generator.py @@ -2,7 +2,7 @@ import json import logging import re from collections.abc import Sequence -from typing import Protocol, cast +from typing import Protocol, TypedDict, cast import json_repair from graphon.enums import WorkflowNodeExecutionMetadataKey @@ -49,6 +49,17 @@ class WorkflowServiceInterface(Protocol): pass +class CodeGenerateResultDict(TypedDict): + code: str + language: str + error: str + + +class StructuredOutputResultDict(TypedDict): + output: str + error: str + + class LLMGenerator: @classmethod def generate_conversation_name( @@ -293,7 +304,7 @@ class LLMGenerator: cls, tenant_id: str, args: RuleCodeGeneratePayload, - ): + ) -> CodeGenerateResultDict: if args.code_language == "python": prompt_template = PromptTemplateParser(PYTHON_CODE_GENERATOR_PROMPT_TEMPLATE) else: @@ -362,7 +373,9 @@ class LLMGenerator: return answer.strip() @classmethod - def generate_structured_output(cls, tenant_id: str, args: RuleStructuredOutputPayload): + def generate_structured_output( + cls, tenant_id: str, args: RuleStructuredOutputPayload + ) -> StructuredOutputResultDict: model_manager = ModelManager.for_tenant(tenant_id=tenant_id) model_instance = model_manager.get_model_instance( tenant_id=tenant_id, @@ -454,7 +467,7 @@ class LLMGenerator: ): session = db.session() - app: App | None = session.query(App).where(App.id == flow_id).first() + app: App | None = session.scalar(select(App).where(App.id == flow_id).limit(1)) if not app: raise ValueError("App not found.") workflow = workflow_service.get_draft_workflow(app_model=app) diff --git a/api/core/logging/filters.py b/api/core/logging/filters.py index 1e8aa8d566..dee1432363 100644 --- a/api/core/logging/filters.py +++ b/api/core/logging/filters.py @@ -6,6 +6,7 @@ import logging import flask from core.logging.context import get_request_id, get_trace_id +from core.logging.structured_formatter import IdentityDict class TraceContextFilter(logging.Filter): @@ -60,7 +61,7 @@ class IdentityContextFilter(logging.Filter): record.user_type = identity.get("user_type", "") return True - def _extract_identity(self) -> dict[str, str]: + def _extract_identity(self) -> IdentityDict: """Extract identity from current_user if in request context.""" try: if not flask.has_request_context(): @@ -77,7 +78,7 @@ class IdentityContextFilter(logging.Filter): from models import Account from models.model import EndUser - identity: dict[str, str] = {} + identity: IdentityDict = {} if isinstance(user, Account): if user.current_tenant_id: diff --git a/api/core/logging/structured_formatter.py b/api/core/logging/structured_formatter.py index 4295d2dd34..9baf6c4682 100644 --- a/api/core/logging/structured_formatter.py +++ b/api/core/logging/structured_formatter.py @@ -3,13 +3,19 @@ import logging import traceback from datetime import UTC, datetime -from typing import Any +from typing import Any, TypedDict import orjson from configs import dify_config +class IdentityDict(TypedDict, total=False): + tenant_id: str + user_id: str + user_type: str + + class StructuredJSONFormatter(logging.Formatter): """ JSON log formatter following the specified schema: @@ -84,7 +90,7 @@ class StructuredJSONFormatter(logging.Formatter): return log_dict - def _extract_identity(self, record: logging.LogRecord) -> dict[str, str] | None: + def _extract_identity(self, record: logging.LogRecord) -> IdentityDict | None: tenant_id = getattr(record, "tenant_id", None) user_id = getattr(record, "user_id", None) user_type = getattr(record, "user_type", None) @@ -92,7 +98,7 @@ class StructuredJSONFormatter(logging.Formatter): if not any([tenant_id, user_id, user_type]): return None - identity: dict[str, str] = {} + identity: IdentityDict = {} if tenant_id: identity["tenant_id"] = tenant_id if user_id: diff --git a/api/core/mcp/server/streamable_http.py b/api/core/mcp/server/streamable_http.py index 278add8cc9..8de002ae55 100644 --- a/api/core/mcp/server/streamable_http.py +++ b/api/core/mcp/server/streamable_http.py @@ -1,7 +1,7 @@ import json import logging from collections.abc import Mapping -from typing import Any, cast +from typing import Any, NotRequired, TypedDict, cast from graphon.variables.input_entities import VariableEntity, VariableEntityType @@ -15,6 +15,17 @@ from services.app_generate_service import AppGenerateService logger = logging.getLogger(__name__) +class ToolParameterSchemaDict(TypedDict): + type: str + properties: dict[str, Any] + required: list[str] + + +class ToolArgumentsDict(TypedDict): + query: NotRequired[str] + inputs: dict[str, Any] + + def handle_mcp_request( app: App, request: mcp_types.ClientRequest, @@ -119,7 +130,7 @@ def handle_list_tools( mcp_types.Tool( name=app_name, description=description, - inputSchema=parameter_schema, + inputSchema=cast(dict[str, Any], parameter_schema), ) ], ) @@ -154,7 +165,7 @@ def build_parameter_schema( app_mode: str, user_input_form: list[VariableEntity], parameters_dict: dict[str, str], -) -> dict[str, Any]: +) -> ToolParameterSchemaDict: """Build parameter schema for the tool""" parameters, required = convert_input_form_to_parameters(user_input_form, parameters_dict) @@ -174,7 +185,7 @@ def build_parameter_schema( } -def prepare_tool_arguments(app: App, arguments: dict[str, Any]) -> dict[str, Any]: +def prepare_tool_arguments(app: App, arguments: dict[str, Any]) -> ToolArgumentsDict: """Prepare arguments based on app mode""" if app.mode == AppMode.WORKFLOW: return {"inputs": arguments} diff --git a/api/core/mcp/session/base_session.py b/api/core/mcp/session/base_session.py index e50fd42198..0b3aa79838 100644 --- a/api/core/mcp/session/base_session.py +++ b/api/core/mcp/session/base_session.py @@ -4,7 +4,7 @@ from collections.abc import Callable from concurrent.futures import Future, ThreadPoolExecutor, TimeoutError from datetime import timedelta from types import TracebackType -from typing import Any, Self, cast +from typing import Any, Self from httpx import HTTPStatusError from pydantic import BaseModel @@ -338,12 +338,11 @@ class BaseSession[ validated_request = self._receive_request_type.model_validate( message.message.root.model_dump(by_alias=True, mode="json", exclude_none=True) ) - validated_request = cast(ReceiveRequestT, validated_request) responder = RequestResponder[ReceiveRequestT, SendResultT]( request_id=message.message.root.id, request_meta=validated_request.root.params.meta if validated_request.root.params else None, - request=validated_request, + request=validated_request, # type: ignore[arg-type] # mypy can't narrow constrained TypeVar from model_validate session=self, on_complete=lambda r: self._in_flight.pop(r.request_id, None), ) @@ -359,15 +358,14 @@ class BaseSession[ notification = self._receive_notification_type.model_validate( message.message.root.model_dump(by_alias=True, mode="json", exclude_none=True) ) - notification = cast(ReceiveNotificationT, notification) # Handle cancellation notifications if isinstance(notification.root, CancelledNotification): cancelled_id = notification.root.params.requestId if cancelled_id in self._in_flight: self._in_flight[cancelled_id].cancel() else: - self._received_notification(notification) - self._handle_incoming(notification) + self._received_notification(notification) # type: ignore[arg-type] + self._handle_incoming(notification) # type: ignore[arg-type] except Exception as e: # For other validation errors, log and continue logger.warning("Failed to validate notification: %s. Message was: %s", e, message.message.root) diff --git a/api/core/model_manager.py b/api/core/model_manager.py index 87d1d7fba6..7a214777bc 100644 --- a/api/core/model_manager.py +++ b/api/core/model_manager.py @@ -17,6 +17,7 @@ from graphon.model_runtime.model_providers.__base.text_embedding_model import Te from graphon.model_runtime.model_providers.__base.tts_model import TTSModel from configs import dify_config +from core.entities import PluginCredentialType from core.entities.embedding_type import EmbeddingInputType from core.entities.provider_configuration import ProviderConfiguration, ProviderModelBundle from core.entities.provider_entities import ModelLoadBalancingConfiguration @@ -25,7 +26,6 @@ from core.plugin.impl.model_runtime_factory import create_plugin_provider_manage from core.provider_manager import ProviderManager from extensions.ext_redis import redis_client from models.provider import ProviderType -from services.enterprise.plugin_manager_service import PluginCredentialType logger = logging.getLogger(__name__) diff --git a/api/core/ops/aliyun_trace/utils.py b/api/core/ops/aliyun_trace/utils.py index d8e105d6a3..aa35ac74c2 100644 --- a/api/core/ops/aliyun_trace/utils.py +++ b/api/core/ops/aliyun_trace/utils.py @@ -1,6 +1,6 @@ import json from collections.abc import Mapping -from typing import Any +from typing import Any, TypedDict from graphon.entities import WorkflowNodeExecution from graphon.enums import WorkflowNodeExecutionStatus @@ -56,10 +56,22 @@ def create_links_from_trace_id(trace_id: str | None) -> list[Link]: return links -def extract_retrieval_documents(documents: list[Document]) -> list[dict[str, Any]]: - documents_data = [] +class RetrievalDocumentMetadataDict(TypedDict): + dataset_id: Any + doc_id: Any + document_id: Any + + +class RetrievalDocumentDict(TypedDict): + content: str + metadata: RetrievalDocumentMetadataDict + score: Any + + +def extract_retrieval_documents(documents: list[Document]) -> list[RetrievalDocumentDict]: + documents_data: list[RetrievalDocumentDict] = [] for document in documents: - document_data = { + document_data: RetrievalDocumentDict = { "content": document.page_content, "metadata": { "dataset_id": document.metadata.get("dataset_id"), @@ -83,7 +95,7 @@ def create_common_span_attributes( framework: str = DEFAULT_FRAMEWORK_NAME, inputs: str = "", outputs: str = "", -) -> dict[str, Any]: +) -> dict[str, str]: return { GEN_AI_SESSION_ID: session_id, GEN_AI_USER_ID: user_id, diff --git a/api/core/ops/base_trace_instance.py b/api/core/ops/base_trace_instance.py index 8c081ae225..a1f96b9edf 100644 --- a/api/core/ops/base_trace_instance.py +++ b/api/core/ops/base_trace_instance.py @@ -56,8 +56,10 @@ class BaseTraceInstance(ABC): if not service_account: raise ValueError(f"Creator account with id {app.created_by} not found for app {app_id}") - current_tenant = ( - session.query(TenantAccountJoin).filter_by(account_id=service_account.id, current=True).first() + current_tenant = session.scalar( + select(TenantAccountJoin) + .where(TenantAccountJoin.account_id == service_account.id, TenantAccountJoin.current.is_(True)) + .limit(1) ) if not current_tenant: raise ValueError(f"Current tenant not found for account {service_account.id}") diff --git a/api/core/ops/tencent_trace/tencent_trace.py b/api/core/ops/tencent_trace/tencent_trace.py index 2bd6db22bf..84f54d8a5a 100644 --- a/api/core/ops/tencent_trace/tencent_trace.py +++ b/api/core/ops/tencent_trace/tencent_trace.py @@ -241,8 +241,10 @@ class TencentDataTrace(BaseTraceInstance): if not service_account: raise ValueError(f"Creator account not found for app {app_id}") - current_tenant = ( - session.query(TenantAccountJoin).filter_by(account_id=service_account.id, current=True).first() + current_tenant = session.scalar( + select(TenantAccountJoin) + .where(TenantAccountJoin.account_id == service_account.id, TenantAccountJoin.current.is_(True)) + .limit(1) ) if not current_tenant: raise ValueError(f"Current tenant not found for account {service_account.id}") diff --git a/api/core/plugin/entities/__init__.py b/api/core/plugin/entities/__init__.py new file mode 100644 index 0000000000..9456ff0181 --- /dev/null +++ b/api/core/plugin/entities/__init__.py @@ -0,0 +1,5 @@ +from core.plugin.entities.oauth import OAuthSchema + +__all__ = [ + "OAuthSchema", +] diff --git a/api/core/plugin/entities/oauth.py b/api/core/plugin/entities/oauth.py index d284b82728..483ebbc535 100644 --- a/api/core/plugin/entities/oauth.py +++ b/api/core/plugin/entities/oauth.py @@ -1,5 +1,3 @@ -from collections.abc import Sequence - from pydantic import BaseModel, Field from core.entities.provider_entities import ProviderConfig @@ -10,12 +8,12 @@ class OAuthSchema(BaseModel): OAuth schema """ - client_schema: Sequence[ProviderConfig] = Field( + client_schema: list[ProviderConfig] = Field( default_factory=list, description="client schema like client_id, client_secret, etc.", ) - credentials_schema: Sequence[ProviderConfig] = Field( + credentials_schema: list[ProviderConfig] = Field( default_factory=list, description="credentials schema like access_token, refresh_token, etc.", ) diff --git a/api/core/provider_manager.py b/api/core/provider_manager.py index 5d536e0e32..552de66f8b 100644 --- a/api/core/provider_manager.py +++ b/api/core/provider_manager.py @@ -1,11 +1,10 @@ from __future__ import annotations import contextlib -import json from collections import defaultdict from collections.abc import Sequence from json import JSONDecodeError -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any from graphon.model_runtime.entities.model_entities import ModelType from graphon.model_runtime.entities.provider_entities import ( @@ -15,6 +14,7 @@ from graphon.model_runtime.entities.provider_entities import ( ProviderEntity, ) from graphon.model_runtime.model_providers.model_provider_factory import ModelProviderFactory +from pydantic import TypeAdapter from sqlalchemy import select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session @@ -58,6 +58,8 @@ from services.feature_service import FeatureService if TYPE_CHECKING: from graphon.model_runtime.runtime import ModelRuntime +_credentials_adapter: TypeAdapter[dict[str, Any]] = TypeAdapter(dict[str, Any]) + class ProviderManager: """ @@ -875,8 +877,8 @@ class ProviderManager: return {"openai_api_key": encrypted_config} try: - credentials = cast(dict, json.loads(encrypted_config)) - except JSONDecodeError: + credentials = _credentials_adapter.validate_json(encrypted_config) + except (ValueError, JSONDecodeError): return {} # Decrypt secret variables @@ -1015,7 +1017,7 @@ class ProviderManager: if not cached_provider_credentials: provider_credentials: dict[str, Any] = {} if provider_records and provider_records[0].encrypted_config: - provider_credentials = json.loads(provider_records[0].encrypted_config) + provider_credentials = _credentials_adapter.validate_json(provider_records[0].encrypted_config) # Get provider credential secret variables provider_credential_secret_variables = self._extract_secret_variables( @@ -1162,8 +1164,10 @@ class ProviderManager: if not cached_provider_model_credentials: try: - provider_model_credentials = json.loads(load_balancing_model_config.encrypted_config) - except JSONDecodeError: + provider_model_credentials = _credentials_adapter.validate_json( + load_balancing_model_config.encrypted_config + ) + except (ValueError, JSONDecodeError): continue # Get decoding rsa key and cipher for decrypting credentials @@ -1176,7 +1180,7 @@ class ProviderManager: if variable in provider_model_credentials: try: provider_model_credentials[variable] = encrypter.decrypt_token_with_decoding( - provider_model_credentials.get(variable), + provider_model_credentials.get(variable) or "", self.decoding_rsa_key, self.decoding_cipher_rsa, ) diff --git a/api/core/rag/datasource/retrieval_service.py b/api/core/rag/datasource/retrieval_service.py index fcbc3ffbfa..c1654ac130 100644 --- a/api/core/rag/datasource/retrieval_service.py +++ b/api/core/rag/datasource/retrieval_service.py @@ -15,7 +15,7 @@ from core.rag.data_post_processor.data_post_processor import DataPostProcessor, from core.rag.datasource.keyword.keyword_factory import Keyword from core.rag.datasource.vdb.vector_factory import Vector from core.rag.embedding.retrieval import AttachmentInfoDict, RetrievalChildChunk, RetrievalSegments -from core.rag.entities.metadata_entities import MetadataCondition +from core.rag.entities import MetadataFilteringCondition from core.rag.index_processor.constant.doc_type import DocType from core.rag.index_processor.constant.index_type import IndexStructureType from core.rag.index_processor.constant.query_type import QueryType @@ -182,7 +182,9 @@ class RetrievalService: if not dataset: return [] metadata_condition = ( - MetadataCondition.model_validate(metadata_filtering_conditions) if metadata_filtering_conditions else None + MetadataFilteringCondition.model_validate(metadata_filtering_conditions) + if metadata_filtering_conditions + else None ) all_documents = ExternalDatasetService.fetch_external_knowledge_retrieval( dataset.tenant_id, @@ -240,7 +242,7 @@ class RetrievalService: @classmethod def _get_dataset(cls, dataset_id: str) -> Dataset | None: with Session(db.engine) as session: - return session.query(Dataset).where(Dataset.id == dataset_id).first() + return session.scalar(select(Dataset).where(Dataset.id == dataset_id).limit(1)) @classmethod def keyword_search( @@ -573,15 +575,13 @@ class RetrievalService: # Batch query summaries for segments retrieved via summary (only enabled summaries) if summary_segment_ids: - summaries = ( - session.query(DocumentSegmentSummary) - .filter( + summaries = session.scalars( + select(DocumentSegmentSummary).where( DocumentSegmentSummary.chunk_id.in_(list(summary_segment_ids)), DocumentSegmentSummary.status == "completed", - DocumentSegmentSummary.enabled == True, # Only retrieve enabled summaries + DocumentSegmentSummary.enabled.is_(True), # Only retrieve enabled summaries ) - .all() - ) + ).all() for summary in summaries: if summary.summary_content: segment_summary_map[summary.chunk_id] = summary.summary_content @@ -851,12 +851,12 @@ class RetrievalService: def get_segment_attachment_info( cls, dataset_id: str, tenant_id: str, attachment_id: str, session: Session ) -> SegmentAttachmentResult | None: - upload_file = session.query(UploadFile).where(UploadFile.id == attachment_id).first() + upload_file = session.scalar(select(UploadFile).where(UploadFile.id == attachment_id).limit(1)) if upload_file: - attachment_binding = ( - session.query(SegmentAttachmentBinding) + attachment_binding = session.scalar( + select(SegmentAttachmentBinding) .where(SegmentAttachmentBinding.attachment_id == upload_file.id) - .first() + .limit(1) ) if attachment_binding: attachment_info: AttachmentInfoDict = { @@ -875,14 +875,12 @@ class RetrievalService: cls, attachment_ids: list[str], session: Session ) -> list[SegmentAttachmentInfoResult]: attachment_infos: list[SegmentAttachmentInfoResult] = [] - upload_files = session.query(UploadFile).where(UploadFile.id.in_(attachment_ids)).all() + upload_files = session.scalars(select(UploadFile).where(UploadFile.id.in_(attachment_ids))).all() if upload_files: upload_file_ids = [upload_file.id for upload_file in upload_files] - attachment_bindings = ( - session.query(SegmentAttachmentBinding) - .where(SegmentAttachmentBinding.attachment_id.in_(upload_file_ids)) - .all() - ) + attachment_bindings = session.scalars( + select(SegmentAttachmentBinding).where(SegmentAttachmentBinding.attachment_id.in_(upload_file_ids)) + ).all() attachment_binding_map = {binding.attachment_id: binding for binding in attachment_bindings} if attachment_bindings: diff --git a/api/core/rag/datasource/vdb/analyticdb/analyticdb_vector_openapi.py b/api/core/rag/datasource/vdb/analyticdb/analyticdb_vector_openapi.py index ce626bbd7e..fb6eaa370a 100644 --- a/api/core/rag/datasource/vdb/analyticdb/analyticdb_vector_openapi.py +++ b/api/core/rag/datasource/vdb/analyticdb/analyticdb_vector_openapi.py @@ -1,5 +1,5 @@ import json -from typing import Any +from typing import Any, TypedDict from pydantic import BaseModel, model_validator @@ -13,6 +13,13 @@ from core.rag.models.document import Document from extensions.ext_redis import redis_client +class AnalyticdbClientParamsDict(TypedDict): + access_key_id: str + access_key_secret: str + region_id: str + read_timeout: int + + class AnalyticdbVectorOpenAPIConfig(BaseModel): access_key_id: str access_key_secret: str @@ -44,13 +51,14 @@ class AnalyticdbVectorOpenAPIConfig(BaseModel): raise ValueError("config ANALYTICDB_NAMESPACE_PASSWORD is required") return values - def to_analyticdb_client_params(self): - return { + def to_analyticdb_client_params(self) -> AnalyticdbClientParamsDict: + result: AnalyticdbClientParamsDict = { "access_key_id": self.access_key_id, "access_key_secret": self.access_key_secret, "region_id": self.region_id, "read_timeout": self.read_timeout, } + return result class AnalyticdbVectorOpenAPI: diff --git a/api/core/rag/datasource/vdb/baidu/baidu_vector.py b/api/core/rag/datasource/vdb/baidu/baidu_vector.py index 2b220fc04d..99ab0d82f2 100644 --- a/api/core/rag/datasource/vdb/baidu/baidu_vector.py +++ b/api/core/rag/datasource/vdb/baidu/baidu_vector.py @@ -30,7 +30,7 @@ from pymochow.model.table import AnnSearch, BM25SearchRequest, HNSWSearchParams, from configs import dify_config from core.rag.datasource.vdb.field import Field as VDBField from core.rag.datasource.vdb.field import parse_metadata_json -from core.rag.datasource.vdb.vector_base import BaseVector +from core.rag.datasource.vdb.vector_base import BaseVector, VectorIndexStructDict from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory from core.rag.datasource.vdb.vector_type import VectorType from core.rag.embedding.embedding_base import Embeddings @@ -85,8 +85,12 @@ class BaiduVector(BaseVector): def get_type(self) -> str: return VectorType.BAIDU - def to_index_struct(self): - return {"type": self.get_type(), "vector_store": {"class_prefix": self._collection_name}} + def to_index_struct(self) -> VectorIndexStructDict: + result: VectorIndexStructDict = { + "type": self.get_type(), + "vector_store": {"class_prefix": self._collection_name}, + } + return result def create(self, texts: list[Document], embeddings: list[list[float]], **kwargs): self._create_table(len(embeddings[0])) diff --git a/api/core/rag/datasource/vdb/chroma/chroma_vector.py b/api/core/rag/datasource/vdb/chroma/chroma_vector.py index cbc846f716..73787c2f00 100644 --- a/api/core/rag/datasource/vdb/chroma/chroma_vector.py +++ b/api/core/rag/datasource/vdb/chroma/chroma_vector.py @@ -1,12 +1,12 @@ import json -from typing import Any +from typing import Any, TypedDict import chromadb from chromadb import QueryResult, Settings from pydantic import BaseModel from configs import dify_config -from core.rag.datasource.vdb.vector_base import BaseVector +from core.rag.datasource.vdb.vector_base import BaseVector, VectorIndexStructDict from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory from core.rag.datasource.vdb.vector_type import VectorType from core.rag.embedding.embedding_base import Embeddings @@ -15,6 +15,15 @@ from extensions.ext_redis import redis_client from models.dataset import Dataset +class ChromaParamsDict(TypedDict): + host: str + port: int + ssl: bool + tenant: str + database: str + settings: Settings + + class ChromaConfig(BaseModel): host: str port: int @@ -23,14 +32,13 @@ class ChromaConfig(BaseModel): auth_provider: str | None = None auth_credentials: str | None = None - def to_chroma_params(self): + def to_chroma_params(self) -> ChromaParamsDict: settings = Settings( # auth chroma_client_auth_provider=self.auth_provider, chroma_client_auth_credentials=self.auth_credentials, ) - - return { + result: ChromaParamsDict = { "host": self.host, "port": self.port, "ssl": False, @@ -38,6 +46,7 @@ class ChromaConfig(BaseModel): "database": self.database, "settings": settings, } + return result class ChromaVector(BaseVector): @@ -145,7 +154,10 @@ class ChromaVectorFactory(AbstractVectorFactory): else: dataset_id = dataset.id collection_name = Dataset.gen_collection_name_by_id(dataset_id).lower() - index_struct_dict = {"type": VectorType.CHROMA, "vector_store": {"class_prefix": collection_name}} + index_struct_dict: VectorIndexStructDict = { + "type": VectorType.CHROMA, + "vector_store": {"class_prefix": collection_name}, + } dataset.index_struct = json.dumps(index_struct_dict) return ChromaVector( diff --git a/api/core/rag/datasource/vdb/milvus/milvus_vector.py b/api/core/rag/datasource/vdb/milvus/milvus_vector.py index 96eb465401..7cdb2d3a99 100644 --- a/api/core/rag/datasource/vdb/milvus/milvus_vector.py +++ b/api/core/rag/datasource/vdb/milvus/milvus_vector.py @@ -1,6 +1,6 @@ import json import logging -from typing import Any +from typing import Any, TypedDict from packaging import version from pydantic import BaseModel, model_validator @@ -20,6 +20,15 @@ from models.dataset import Dataset logger = logging.getLogger(__name__) +class MilvusParamsDict(TypedDict): + uri: str + token: str | None + user: str | None + password: str | None + db_name: str + analyzer_params: str | None + + class MilvusConfig(BaseModel): """ Configuration class for Milvus connection. @@ -50,11 +59,11 @@ class MilvusConfig(BaseModel): raise ValueError("config MILVUS_PASSWORD is required") return values - def to_milvus_params(self): + def to_milvus_params(self) -> MilvusParamsDict: """ Convert the configuration to a dictionary of Milvus connection parameters. """ - return { + result: MilvusParamsDict = { "uri": self.uri, "token": self.token, "user": self.user, @@ -62,6 +71,7 @@ class MilvusConfig(BaseModel): "db_name": self.database, "analyzer_params": self.analyzer_params, } + return result class MilvusVector(BaseVector): @@ -352,6 +362,7 @@ class MilvusVector(BaseVector): # Create Index params for the collection index_params_obj = IndexParams() + assert index_params is not None index_params_obj.add_index(field_name=Field.VECTOR, **index_params) # Create Sparse Vector Index for the collection diff --git a/api/core/rag/datasource/vdb/qdrant/qdrant_vector.py b/api/core/rag/datasource/vdb/qdrant/qdrant_vector.py index a9f946dd43..f4fcb975c3 100644 --- a/api/core/rag/datasource/vdb/qdrant/qdrant_vector.py +++ b/api/core/rag/datasource/vdb/qdrant/qdrant_vector.py @@ -22,7 +22,7 @@ from sqlalchemy import select from configs import dify_config from core.rag.datasource.vdb.field import Field -from core.rag.datasource.vdb.vector_base import BaseVector +from core.rag.datasource.vdb.vector_base import BaseVector, VectorIndexStructDict from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory from core.rag.datasource.vdb.vector_type import VectorType from core.rag.embedding.embedding_base import Embeddings @@ -94,8 +94,12 @@ class QdrantVector(BaseVector): def get_type(self) -> str: return VectorType.QDRANT - def to_index_struct(self): - return {"type": self.get_type(), "vector_store": {"class_prefix": self._collection_name}} + def to_index_struct(self) -> VectorIndexStructDict: + result: VectorIndexStructDict = { + "type": self.get_type(), + "vector_store": {"class_prefix": self._collection_name}, + } + return result def create(self, texts: list[Document], embeddings: list[list[float]], **kwargs): if texts: diff --git a/api/core/rag/datasource/vdb/tencent/tencent_vector.py b/api/core/rag/datasource/vdb/tencent/tencent_vector.py index 829db9db20..2f26d6fff3 100644 --- a/api/core/rag/datasource/vdb/tencent/tencent_vector.py +++ b/api/core/rag/datasource/vdb/tencent/tencent_vector.py @@ -1,7 +1,7 @@ import json import logging import math -from typing import Any +from typing import Any, TypedDict from pydantic import BaseModel from tcvdb_text.encoder import BM25Encoder # type: ignore @@ -12,7 +12,7 @@ from tcvectordb.model.document import AnnSearch, Filter, KeywordSearch, Weighted from configs import dify_config from core.rag.datasource.vdb.field import parse_metadata_json -from core.rag.datasource.vdb.vector_base import BaseVector +from core.rag.datasource.vdb.vector_base import BaseVector, VectorIndexStructDict from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory from core.rag.datasource.vdb.vector_type import VectorType from core.rag.embedding.embedding_base import Embeddings @@ -23,6 +23,13 @@ from models.dataset import Dataset logger = logging.getLogger(__name__) +class TencentParamsDict(TypedDict): + url: str + username: str | None + key: str | None + timeout: float + + class TencentConfig(BaseModel): url: str api_key: str | None = None @@ -36,8 +43,14 @@ class TencentConfig(BaseModel): max_upsert_batch_size: int = 128 enable_hybrid_search: bool = False # Flag to enable hybrid search - def to_tencent_params(self): - return {"url": self.url, "username": self.username, "key": self.api_key, "timeout": self.timeout} + def to_tencent_params(self) -> TencentParamsDict: + result: TencentParamsDict = { + "url": self.url, + "username": self.username, + "key": self.api_key, + "timeout": self.timeout, + } + return result bm25 = BM25Encoder.default("zh") @@ -83,8 +96,12 @@ class TencentVector(BaseVector): def get_type(self) -> str: return VectorType.TENCENT - def to_index_struct(self): - return {"type": self.get_type(), "vector_store": {"class_prefix": self._collection_name}} + def to_index_struct(self) -> VectorIndexStructDict: + result: VectorIndexStructDict = { + "type": self.get_type(), + "vector_store": {"class_prefix": self._collection_name}, + } + return result def _has_collection(self) -> bool: return bool( diff --git a/api/core/rag/datasource/vdb/tidb_on_qdrant/tidb_on_qdrant_vector.py b/api/core/rag/datasource/vdb/tidb_on_qdrant/tidb_on_qdrant_vector.py index 499a48ac76..605cc5a08f 100644 --- a/api/core/rag/datasource/vdb/tidb_on_qdrant/tidb_on_qdrant_vector.py +++ b/api/core/rag/datasource/vdb/tidb_on_qdrant/tidb_on_qdrant_vector.py @@ -25,7 +25,7 @@ from sqlalchemy import select from configs import dify_config from core.rag.datasource.vdb.field import Field from core.rag.datasource.vdb.tidb_on_qdrant.tidb_service import TidbService -from core.rag.datasource.vdb.vector_base import BaseVector +from core.rag.datasource.vdb.vector_base import BaseVector, VectorIndexStructDict from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory from core.rag.datasource.vdb.vector_type import VectorType from core.rag.embedding.embedding_base import Embeddings @@ -91,8 +91,12 @@ class TidbOnQdrantVector(BaseVector): def get_type(self) -> str: return VectorType.TIDB_ON_QDRANT - def to_index_struct(self): - return {"type": self.get_type(), "vector_store": {"class_prefix": self._collection_name}} + def to_index_struct(self) -> VectorIndexStructDict: + result: VectorIndexStructDict = { + "type": self.get_type(), + "vector_store": {"class_prefix": self._collection_name}, + } + return result def create(self, texts: list[Document], embeddings: list[list[float]], **kwargs): if texts: diff --git a/api/core/rag/datasource/vdb/vector_base.py b/api/core/rag/datasource/vdb/vector_base.py index f29b270e40..6fbd802a10 100644 --- a/api/core/rag/datasource/vdb/vector_base.py +++ b/api/core/rag/datasource/vdb/vector_base.py @@ -1,11 +1,20 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import Any +from typing import Any, TypedDict from core.rag.models.document import Document +class VectorStoreDict(TypedDict): + class_prefix: str + + +class VectorIndexStructDict(TypedDict): + type: str + vector_store: VectorStoreDict + + class BaseVector(ABC): def __init__(self, collection_name: str): self._collection_name = collection_name diff --git a/api/core/rag/datasource/vdb/vector_factory.py b/api/core/rag/datasource/vdb/vector_factory.py index 26531eab88..0ef88e1010 100644 --- a/api/core/rag/datasource/vdb/vector_factory.py +++ b/api/core/rag/datasource/vdb/vector_factory.py @@ -9,7 +9,7 @@ from sqlalchemy import select from configs import dify_config from core.model_manager import ModelManager -from core.rag.datasource.vdb.vector_base import BaseVector +from core.rag.datasource.vdb.vector_base import BaseVector, VectorIndexStructDict from core.rag.datasource.vdb.vector_type import VectorType from core.rag.embedding.cached_embedding import CacheEmbedding from core.rag.embedding.embedding_base import Embeddings @@ -30,8 +30,11 @@ class AbstractVectorFactory(ABC): raise NotImplementedError @staticmethod - def gen_index_struct_dict(vector_type: VectorType, collection_name: str): - index_struct_dict = {"type": vector_type, "vector_store": {"class_prefix": collection_name}} + def gen_index_struct_dict(vector_type: VectorType, collection_name: str) -> VectorIndexStructDict: + index_struct_dict: VectorIndexStructDict = { + "type": vector_type, + "vector_store": {"class_prefix": collection_name}, + } return index_struct_dict diff --git a/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py b/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py index d29d62c93f..25b65b82a9 100644 --- a/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py +++ b/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py @@ -24,7 +24,7 @@ from weaviate.exceptions import UnexpectedStatusCodeError from configs import dify_config from core.rag.datasource.vdb.field import Field -from core.rag.datasource.vdb.vector_base import BaseVector +from core.rag.datasource.vdb.vector_base import BaseVector, VectorIndexStructDict from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory from core.rag.datasource.vdb.vector_type import VectorType from core.rag.embedding.embedding_base import Embeddings @@ -184,9 +184,13 @@ class WeaviateVector(BaseVector): dataset_id = dataset.id return Dataset.gen_collection_name_by_id(dataset_id) - def to_index_struct(self) -> dict: + def to_index_struct(self) -> VectorIndexStructDict: """Returns the index structure dictionary for persistence.""" - return {"type": self.get_type(), "vector_store": {"class_prefix": self._collection_name}} + result: VectorIndexStructDict = { + "type": self.get_type(), + "vector_store": {"class_prefix": self._collection_name}, + } + return result def create(self, texts: list[Document], embeddings: list[list[float]], **kwargs): """ diff --git a/api/core/rag/entities/__init__.py b/api/core/rag/entities/__init__.py new file mode 100644 index 0000000000..63c6708704 --- /dev/null +++ b/api/core/rag/entities/__init__.py @@ -0,0 +1,28 @@ +from core.rag.entities.citation_metadata import RetrievalSourceMetadata +from core.rag.entities.context_entities import DocumentContext +from core.rag.entities.event import DatasourceCompletedEvent, DatasourceErrorEvent, DatasourceProcessingEvent +from core.rag.entities.index_entities import EconomySetting, EmbeddingSetting, IndexMethod +from core.rag.entities.metadata_entities import Condition, MetadataFilteringCondition, SupportedComparisonOperator +from core.rag.entities.processing_entities import ParentMode, PreProcessingRule, Rule, Segmentation +from core.rag.entities.retrieval_settings import KeywordSetting, VectorSetting, WeightedScoreConfig + +__all__ = [ + "Condition", + "DatasourceCompletedEvent", + "DatasourceErrorEvent", + "DatasourceProcessingEvent", + "DocumentContext", + "EconomySetting", + "EmbeddingSetting", + "IndexMethod", + "KeywordSetting", + "MetadataFilteringCondition", + "ParentMode", + "PreProcessingRule", + "RetrievalSourceMetadata", + "Rule", + "Segmentation", + "SupportedComparisonOperator", + "VectorSetting", + "WeightedScoreConfig", +] diff --git a/api/core/rag/entities/index_entities.py b/api/core/rag/entities/index_entities.py new file mode 100644 index 0000000000..f86a04fa9f --- /dev/null +++ b/api/core/rag/entities/index_entities.py @@ -0,0 +1,30 @@ +from typing import Literal + +from pydantic import BaseModel + + +class EmbeddingSetting(BaseModel): + """ + Embedding Setting. + """ + + embedding_provider_name: str + embedding_model_name: str + + +class EconomySetting(BaseModel): + """ + Economy Setting. + """ + + keyword_number: int + + +class IndexMethod(BaseModel): + """ + Knowledge Index Setting. + """ + + indexing_technique: Literal["high_quality", "economy"] + embedding_setting: EmbeddingSetting + economy_setting: EconomySetting diff --git a/api/core/rag/entities/metadata_entities.py b/api/core/rag/entities/metadata_entities.py index b07d760cf4..a2ac44807f 100644 --- a/api/core/rag/entities/metadata_entities.py +++ b/api/core/rag/entities/metadata_entities.py @@ -38,9 +38,9 @@ class Condition(BaseModel): value: str | Sequence[str] | None | int | float = None -class MetadataCondition(BaseModel): +class MetadataFilteringCondition(BaseModel): """ - Metadata Condition. + Metadata Filtering Condition. """ logical_operator: Literal["and", "or"] | None = "and" diff --git a/api/core/rag/entities/processing_entities.py b/api/core/rag/entities/processing_entities.py new file mode 100644 index 0000000000..1b54444a19 --- /dev/null +++ b/api/core/rag/entities/processing_entities.py @@ -0,0 +1,27 @@ +from enum import StrEnum +from typing import Literal + +from pydantic import BaseModel + + +class ParentMode(StrEnum): + FULL_DOC = "full-doc" + PARAGRAPH = "paragraph" + + +class PreProcessingRule(BaseModel): + id: str + enabled: bool + + +class Segmentation(BaseModel): + separator: str = "\n" + max_tokens: int + chunk_overlap: int = 0 + + +class Rule(BaseModel): + pre_processing_rules: list[PreProcessingRule] | None = None + segmentation: Segmentation | None = None + parent_mode: Literal["full-doc", "paragraph"] | None = None + subchunk_segmentation: Segmentation | None = None diff --git a/api/core/rag/entities/retrieval_settings.py b/api/core/rag/entities/retrieval_settings.py new file mode 100644 index 0000000000..a0c6512c9c --- /dev/null +++ b/api/core/rag/entities/retrieval_settings.py @@ -0,0 +1,28 @@ +from pydantic import BaseModel + + +class VectorSetting(BaseModel): + """ + Vector Setting. + """ + + vector_weight: float + embedding_provider_name: str + embedding_model_name: str + + +class KeywordSetting(BaseModel): + """ + Keyword Setting. + """ + + keyword_weight: float + + +class WeightedScoreConfig(BaseModel): + """ + Weighted score Config. + """ + + vector_setting: VectorSetting + keyword_setting: KeywordSetting diff --git a/api/core/rag/index_processor/index_processor.py b/api/core/rag/index_processor/index_processor.py index 825ae01226..813a84cbbd 100644 --- a/api/core/rag/index_processor/index_processor.py +++ b/api/core/rag/index_processor/index_processor.py @@ -12,7 +12,7 @@ from core.db.session_factory import session_factory from core.rag.index_processor.constant.index_type import IndexTechniqueType from core.rag.index_processor.index_processor_base import SummaryIndexSettingDict from core.workflow.nodes.knowledge_index.exc import KnowledgeIndexNodeError -from core.workflow.nodes.knowledge_index.protocols import Preview, PreviewItem, QaPreview +from core.workflow.nodes.knowledge_index.protocols import IndexingResultDict, Preview, PreviewItem, QaPreview from models.dataset import Dataset, Document, DocumentSegment from .index_processor_factory import IndexProcessorFactory @@ -61,7 +61,7 @@ class IndexProcessor: chunks: Mapping[str, Any], batch: Any, summary_index_setting: SummaryIndexSettingDict | None = None, - ): + ) -> IndexingResultDict: with session_factory.create_session() as session: document = session.query(Document).filter_by(id=document_id).first() if not document: @@ -129,7 +129,7 @@ class IndexProcessor: } ) - return { + result: IndexingResultDict = { "dataset_id": dataset_id, "dataset_name": dataset_name_value, "batch": batch, @@ -138,6 +138,7 @@ class IndexProcessor: "created_at": created_at_value.timestamp(), "display_status": "completed", } + return result def get_preview_output( self, diff --git a/api/core/rag/index_processor/processor/paragraph_index_processor.py b/api/core/rag/index_processor/processor/paragraph_index_processor.py index 22ab492cbf..4a731bf277 100644 --- a/api/core/rag/index_processor/processor/paragraph_index_processor.py +++ b/api/core/rag/index_processor/processor/paragraph_index_processor.py @@ -32,6 +32,7 @@ from core.rag.datasource.keyword.keyword_factory import Keyword from core.rag.datasource.retrieval_service import RetrievalService from core.rag.datasource.vdb.vector_factory import Vector from core.rag.docstore.dataset_docstore import DatasetDocumentStore +from core.rag.entities import Rule from core.rag.extractor.entity.extract_setting import ExtractSetting from core.rag.extractor.extract_processor import ExtractProcessor from core.rag.index_processor.constant.doc_type import DocType @@ -49,7 +50,6 @@ from models.account import Account from models.dataset import Dataset, DatasetProcessRule, DocumentSegment, SegmentAttachmentBinding from models.dataset import Document as DatasetDocument from services.account_service import AccountService -from services.entities.knowledge_entities.knowledge_entities import Rule from services.summary_index_service import SummaryIndexService _file_access_controller = DatabaseFileAccessController() diff --git a/api/core/rag/index_processor/processor/parent_child_index_processor.py b/api/core/rag/index_processor/processor/parent_child_index_processor.py index 1c5e02e9c8..53596b5de8 100644 --- a/api/core/rag/index_processor/processor/parent_child_index_processor.py +++ b/api/core/rag/index_processor/processor/parent_child_index_processor.py @@ -17,6 +17,7 @@ from core.rag.data_post_processor.data_post_processor import RerankingModelDict from core.rag.datasource.retrieval_service import RetrievalService from core.rag.datasource.vdb.vector_factory import Vector from core.rag.docstore.dataset_docstore import DatasetDocumentStore +from core.rag.entities import ParentMode, Rule from core.rag.extractor.entity.extract_setting import ExtractSetting from core.rag.extractor.extract_processor import ExtractProcessor from core.rag.index_processor.constant.doc_type import DocType @@ -30,7 +31,6 @@ from models import Account from models.dataset import ChildChunk, Dataset, DatasetProcessRule, DocumentSegment from models.dataset import Document as DatasetDocument from services.account_service import AccountService -from services.entities.knowledge_entities.knowledge_entities import ParentMode, Rule from services.summary_index_service import SummaryIndexService logger = logging.getLogger(__name__) diff --git a/api/core/rag/index_processor/processor/qa_index_processor.py b/api/core/rag/index_processor/processor/qa_index_processor.py index 6874603a83..273ea0f852 100644 --- a/api/core/rag/index_processor/processor/qa_index_processor.py +++ b/api/core/rag/index_processor/processor/qa_index_processor.py @@ -19,6 +19,7 @@ from core.rag.data_post_processor.data_post_processor import RerankingModelDict from core.rag.datasource.retrieval_service import RetrievalService from core.rag.datasource.vdb.vector_factory import Vector from core.rag.docstore.dataset_docstore import DatasetDocumentStore +from core.rag.entities import Rule from core.rag.extractor.entity.extract_setting import ExtractSetting from core.rag.extractor.extract_processor import ExtractProcessor from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType @@ -30,7 +31,6 @@ from libs import helper from models.account import Account from models.dataset import Dataset, DocumentSegment from models.dataset import Document as DatasetDocument -from services.entities.knowledge_entities.knowledge_entities import Rule from services.summary_index_service import SummaryIndexService logger = logging.getLogger(__name__) diff --git a/api/core/rag/rerank/entity/weight.py b/api/core/rag/rerank/entity/weight.py index 6dbbad2f8d..54392a0323 100644 --- a/api/core/rag/rerank/entity/weight.py +++ b/api/core/rag/rerank/entity/weight.py @@ -1,16 +1,6 @@ from pydantic import BaseModel - -class VectorSetting(BaseModel): - vector_weight: float - - embedding_provider_name: str - - embedding_model_name: str - - -class KeywordSetting(BaseModel): - keyword_weight: float +from core.rag.entities import KeywordSetting, VectorSetting class Weights(BaseModel): diff --git a/api/core/rag/retrieval/dataset_retrieval.py b/api/core/rag/retrieval/dataset_retrieval.py index 593e1f1420..4e9b53b83e 100644 --- a/api/core/rag/retrieval/dataset_retrieval.py +++ b/api/core/rag/retrieval/dataset_retrieval.py @@ -39,9 +39,7 @@ from core.prompt.simple_prompt_transform import ModelMode from core.rag.data_post_processor.data_post_processor import DataPostProcessor, RerankingModelDict, WeightsDict from core.rag.datasource.keyword.jieba.jieba_keyword_table_handler import JiebaKeywordTableHandler from core.rag.datasource.retrieval_service import DefaultRetrievalModelDict, RetrievalService -from core.rag.entities.citation_metadata import RetrievalSourceMetadata -from core.rag.entities.context_entities import DocumentContext -from core.rag.entities.metadata_entities import Condition, MetadataCondition +from core.rag.entities import Condition, DocumentContext, RetrievalSourceMetadata from core.rag.index_processor.constant.doc_type import DocType from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType from core.rag.index_processor.constant.query_type import QueryType @@ -604,7 +602,7 @@ class DatasetRetrieval: planning_strategy: PlanningStrategy, message_id: str | None = None, metadata_filter_document_ids: dict[str, list[str]] | None = None, - metadata_condition: MetadataCondition | None = None, + metadata_condition: MetadataFilteringCondition | None = None, ): tools = [] for dataset in available_datasets: @@ -743,7 +741,7 @@ class DatasetRetrieval: reranking_enable: bool = True, message_id: str | None = None, metadata_filter_document_ids: dict[str, list[str]] | None = None, - metadata_condition: MetadataCondition | None = None, + metadata_condition: MetadataFilteringCondition | None = None, attachment_ids: list[str] | None = None, ): if not available_datasets: @@ -1063,7 +1061,7 @@ class DatasetRetrieval: top_k: int, all_documents: list[Document], document_ids_filter: list[str] | None = None, - metadata_condition: MetadataCondition | None = None, + metadata_condition: MetadataFilteringCondition | None = None, attachment_ids: list[str] | None = None, ): with flask_app.app_context(): @@ -1339,7 +1337,7 @@ class DatasetRetrieval: metadata_model_config: ModelConfig, metadata_filtering_conditions: MetadataFilteringCondition | None, inputs: dict, - ) -> tuple[dict[str, list[str]] | None, MetadataCondition | None]: + ) -> tuple[dict[str, list[str]] | None, MetadataFilteringCondition | None]: document_query = select(DatasetDocument).where( DatasetDocument.dataset_id.in_(dataset_ids), DatasetDocument.indexing_status == "completed", @@ -1371,7 +1369,7 @@ class DatasetRetrieval: value=filter.get("value"), ) ) - metadata_condition = MetadataCondition( + metadata_condition = MetadataFilteringCondition( logical_operator=metadata_filtering_conditions.logical_operator if metadata_filtering_conditions else "or", # type: ignore @@ -1400,7 +1398,7 @@ class DatasetRetrieval: expected_value, filters, ) - metadata_condition = MetadataCondition( + metadata_condition = MetadataFilteringCondition( logical_operator=metadata_filtering_conditions.logical_operator, conditions=conditions, ) @@ -1723,7 +1721,7 @@ class DatasetRetrieval: self, flask_app: Flask, available_datasets: list[Dataset], - metadata_condition: MetadataCondition | None, + metadata_condition: MetadataFilteringCondition | None, metadata_filter_document_ids: dict[str, list[str]] | None, all_documents: list[Document], tenant_id: str, diff --git a/api/core/tools/entities/common_entities.py b/api/core/tools/entities/common_entities.py index 21d310bbb9..83a042ed63 100644 --- a/api/core/tools/entities/common_entities.py +++ b/api/core/tools/entities/common_entities.py @@ -1,6 +1,15 @@ +from typing import TypedDict + from pydantic import BaseModel, Field, model_validator +class I18nObjectDict(TypedDict): + zh_Hans: str | None + en_US: str + pt_BR: str | None + ja_JP: str | None + + class I18nObject(BaseModel): """ Model class for i18n object. @@ -18,5 +27,11 @@ class I18nObject(BaseModel): self.ja_JP = self.ja_JP or self.en_US return self - def to_dict(self): - return {"zh_Hans": self.zh_Hans, "en_US": self.en_US, "pt_BR": self.pt_BR, "ja_JP": self.ja_JP} + def to_dict(self) -> I18nObjectDict: + result: I18nObjectDict = { + "zh_Hans": self.zh_Hans, + "en_US": self.en_US, + "pt_BR": self.pt_BR, + "ja_JP": self.ja_JP, + } + return result diff --git a/api/core/tools/entities/tool_entities.py b/api/core/tools/entities/tool_entities.py index 96268d029e..31e879add2 100644 --- a/api/core/tools/entities/tool_entities.py +++ b/api/core/tools/entities/tool_entities.py @@ -6,9 +6,20 @@ from collections.abc import Mapping from enum import StrEnum, auto from typing import Any, Union -from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_serializer, field_validator, model_validator +from pydantic import ( + BaseModel, + ConfigDict, + Field, + TypeAdapter, + ValidationInfo, + field_serializer, + field_validator, + model_validator, +) +from typing_extensions import TypedDict from core.entities.provider_entities import ProviderConfig +from core.plugin.entities import OAuthSchema from core.plugin.entities.parameters import ( MCPServerParameterType, PluginParameter, @@ -18,11 +29,19 @@ from core.plugin.entities.parameters import ( cast_parameter_value, init_frontend_parameter, ) -from core.rag.entities.citation_metadata import RetrievalSourceMetadata +from core.rag.entities import RetrievalSourceMetadata from core.tools.entities.common_entities import I18nObject from core.tools.entities.constants import TOOL_SELECTOR_MODEL_IDENTITY +class EmojiIconDict(TypedDict): + background: str + content: str + + +emoji_icon_adapter: TypeAdapter[EmojiIconDict] = TypeAdapter(EmojiIconDict) + + class ToolLabelEnum(StrEnum): SEARCH = "search" IMAGE = "image" @@ -410,15 +429,6 @@ class ToolEntity(BaseModel): return value or {} -class OAuthSchema(BaseModel): - client_schema: list[ProviderConfig] = Field( - default_factory=list[ProviderConfig], description="The schema of the OAuth client" - ) - credentials_schema: list[ProviderConfig] = Field( - default_factory=list[ProviderConfig], description="The schema of the OAuth credentials" - ) - - class ToolProviderEntity(BaseModel): identity: ToolProviderIdentity plugin_id: str | None = None diff --git a/api/core/tools/tool_manager.py b/api/core/tools/tool_manager.py index a58d310313..d45d45c520 100644 --- a/api/core/tools/tool_manager.py +++ b/api/core/tools/tool_manager.py @@ -5,16 +5,19 @@ import time from collections.abc import Generator, Mapping from os import listdir, path from threading import Lock -from typing import TYPE_CHECKING, Any, Literal, Optional, Protocol, TypedDict, Union, cast +from typing import TYPE_CHECKING, Any, Literal, Optional, Protocol, Union, cast import sqlalchemy as sa from graphon.runtime import VariablePool +from pydantic import TypeAdapter from sqlalchemy import select from sqlalchemy.orm import Session +from typing_extensions import TypedDict from yarl import URL import contexts from configs import dify_config +from core.entities import PluginCredentialType from core.helper.provider_cache import ToolProviderCredentialsCache from core.plugin.impl.tool import PluginToolManager from core.tools.__base.tool_provider import ToolProviderController @@ -27,7 +30,6 @@ from core.tools.utils.uuid_utils import is_valid_uuid from core.tools.workflow_as_tool.provider import WorkflowToolProviderController from extensions.ext_database import db from models.provider_ids import ToolProviderID -from services.enterprise.plugin_manager_service import PluginCredentialType from services.tools.mcp_tools_manage_service import MCPToolManageService if TYPE_CHECKING: @@ -49,9 +51,11 @@ from core.tools.entities.api_entities import ToolProviderApiEntity, ToolProvider from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ( ApiProviderAuthType, + EmojiIconDict, ToolInvokeFrom, ToolParameter, ToolProviderType, + emoji_icon_adapter, ) from core.tools.errors import ToolProviderNotFoundError from core.tools.tool_label_manager import ToolLabelManager @@ -72,9 +76,7 @@ class ApiProviderControllerItem(TypedDict): controller: ApiToolProviderController -class EmojiIconDict(TypedDict): - background: str - content: str +_credentials_adapter: TypeAdapter[dict[str, Any]] = TypeAdapter(dict[str, Any]) class WorkflowToolRuntimeSpec(Protocol): @@ -885,7 +887,7 @@ class ToolManager: raise ValueError(f"you have not added provider {provider_name}") try: - credentials = json.loads(provider_obj.credentials_str) or {} + credentials = _credentials_adapter.validate_json(provider_obj.credentials_str) or {} except Exception: credentials = {} @@ -910,7 +912,7 @@ class ToolManager: masked_credentials = encrypter.mask_plugin_credentials(encrypter.decrypt(credentials)) try: - icon = json.loads(provider_obj.icon) + icon = emoji_icon_adapter.validate_json(provider_obj.icon) except Exception: icon = {"background": "#252525", "content": "\ud83d\ude01"} @@ -973,7 +975,7 @@ class ToolManager: if workflow_provider is None: raise ToolProviderNotFoundError(f"workflow provider {provider_id} not found") - icon = json.loads(workflow_provider.icon) + icon = emoji_icon_adapter.validate_json(workflow_provider.icon) return icon except Exception: return {"background": "#252525", "content": "\ud83d\ude01"} @@ -990,7 +992,7 @@ class ToolManager: if api_provider is None: raise ToolProviderNotFoundError(f"api provider {provider_id} not found") - icon = json.loads(api_provider.icon) + icon = emoji_icon_adapter.validate_json(api_provider.icon) return icon except Exception: return {"background": "#252525", "content": "\ud83d\ude01"} diff --git a/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py b/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py index e63435db98..c72bdf02ed 100644 --- a/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py +++ b/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py @@ -8,7 +8,7 @@ from sqlalchemy import select from core.callback_handler.index_tool_callback_handler import DatasetIndexToolCallbackHandler from core.model_manager import ModelManager from core.rag.datasource.retrieval_service import RetrievalService -from core.rag.entities.citation_metadata import RetrievalSourceMetadata +from core.rag.entities import RetrievalSourceMetadata from core.rag.index_processor.constant.index_type import IndexTechniqueType from core.rag.models.document import Document as RagDocument from core.rag.rerank.rerank_model import RerankModelRunner diff --git a/api/core/tools/utils/dataset_retriever/dataset_retriever_tool.py b/api/core/tools/utils/dataset_retriever/dataset_retriever_tool.py index cbd8bdb36c..a346eb53c4 100644 --- a/api/core/tools/utils/dataset_retriever/dataset_retriever_tool.py +++ b/api/core/tools/utils/dataset_retriever/dataset_retriever_tool.py @@ -6,8 +6,7 @@ from sqlalchemy import select from core.app.app_config.entities import DatasetRetrieveConfigEntity, ModelConfig from core.rag.data_post_processor.data_post_processor import RerankingModelDict, WeightsDict from core.rag.datasource.retrieval_service import RetrievalService -from core.rag.entities.citation_metadata import RetrievalSourceMetadata -from core.rag.entities.context_entities import DocumentContext +from core.rag.entities import DocumentContext, RetrievalSourceMetadata from core.rag.index_processor.constant.index_type import IndexTechniqueType from core.rag.models.document import Document as RetrievalDocument from core.rag.retrieval.dataset_retrieval import DatasetRetrieval diff --git a/api/core/trigger/entities/entities.py b/api/core/trigger/entities/entities.py index 89824481b5..a922e881cd 100644 --- a/api/core/trigger/entities/entities.py +++ b/api/core/trigger/entities/entities.py @@ -6,6 +6,7 @@ from typing import Any, Union from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_validator from core.entities.provider_entities import ProviderConfig +from core.plugin.entities import OAuthSchema from core.plugin.entities.parameters import ( PluginParameterAutoGenerate, PluginParameterOption, @@ -108,13 +109,6 @@ class EventEntity(BaseModel): return v or [] -class OAuthSchema(BaseModel): - client_schema: list[ProviderConfig] = Field(default_factory=list, description="The schema of the OAuth client") - credentials_schema: list[ProviderConfig] = Field( - default_factory=list, description="The schema of the OAuth credentials" - ) - - class SubscriptionConstructor(BaseModel): """ The subscription constructor of the trigger provider diff --git a/api/core/workflow/nodes/knowledge_index/entities.py b/api/core/workflow/nodes/knowledge_index/entities.py index cba6c12dca..6ff162973c 100644 --- a/api/core/workflow/nodes/knowledge_index/entities.py +++ b/api/core/workflow/nodes/knowledge_index/entities.py @@ -1,9 +1,10 @@ -from typing import Literal, Union +from typing import Union from graphon.entities.base_node_data import BaseNodeData from graphon.enums import NodeType from pydantic import BaseModel +from core.rag.entities import WeightedScoreConfig from core.rag.index_processor.index_processor_base import SummaryIndexSettingDict from core.rag.retrieval.retrieval_methods import RetrievalMethod from core.workflow.nodes.knowledge_index import KNOWLEDGE_INDEX_NODE_TYPE @@ -18,50 +19,6 @@ class RerankingModelConfig(BaseModel): reranking_model_name: str -class VectorSetting(BaseModel): - """ - Vector Setting. - """ - - vector_weight: float - embedding_provider_name: str - embedding_model_name: str - - -class KeywordSetting(BaseModel): - """ - Keyword Setting. - """ - - keyword_weight: float - - -class WeightedScoreConfig(BaseModel): - """ - Weighted score Config. - """ - - vector_setting: VectorSetting - keyword_setting: KeywordSetting - - -class EmbeddingSetting(BaseModel): - """ - Embedding Setting. - """ - - embedding_provider_name: str - embedding_model_name: str - - -class EconomySetting(BaseModel): - """ - Economy Setting. - """ - - keyword_number: int - - class RetrievalSetting(BaseModel): """ Retrieval Setting. @@ -77,16 +34,6 @@ class RetrievalSetting(BaseModel): weights: WeightedScoreConfig | None = None -class IndexMethod(BaseModel): - """ - Knowledge Index Setting. - """ - - indexing_technique: Literal["high_quality", "economy"] - embedding_setting: EmbeddingSetting - economy_setting: EconomySetting - - class FileInfo(BaseModel): """ File Info. diff --git a/api/core/workflow/nodes/knowledge_index/protocols.py b/api/core/workflow/nodes/knowledge_index/protocols.py index bb52123082..6668f0c98e 100644 --- a/api/core/workflow/nodes/knowledge_index/protocols.py +++ b/api/core/workflow/nodes/knowledge_index/protocols.py @@ -1,9 +1,19 @@ from collections.abc import Mapping -from typing import Any, Protocol +from typing import Any, Protocol, TypedDict from pydantic import BaseModel, Field +class IndexingResultDict(TypedDict): + dataset_id: str + dataset_name: str + batch: Any + document_id: str + document_name: str + created_at: float + display_status: str + + class PreviewItem(BaseModel): content: str | None = Field(default=None) child_chunks: list[str] | None = Field(default=None) @@ -34,7 +44,7 @@ class IndexProcessorProtocol(Protocol): chunks: Mapping[str, Any], batch: Any, summary_index_setting: dict | None = None, - ) -> dict[str, Any]: ... + ) -> IndexingResultDict: ... def get_preview_output( self, chunks: Any, dataset_id: str, document_id: str, chunk_structure: str, summary_index_setting: dict | None diff --git a/api/core/workflow/nodes/knowledge_retrieval/entities.py b/api/core/workflow/nodes/knowledge_retrieval/entities.py index b1fa8593ef..f4bc3fb9d3 100644 --- a/api/core/workflow/nodes/knowledge_retrieval/entities.py +++ b/api/core/workflow/nodes/knowledge_retrieval/entities.py @@ -1,4 +1,3 @@ -from collections.abc import Sequence from typing import Literal from graphon.entities.base_node_data import BaseNodeData @@ -6,6 +5,10 @@ from graphon.enums import BuiltinNodeTypes, NodeType from graphon.nodes.llm.entities import ModelConfig, VisionConfig from pydantic import BaseModel, Field +from core.rag.entities import Condition, MetadataFilteringCondition, WeightedScoreConfig + +__all__ = ["Condition"] + class RerankingModelConfig(BaseModel): """ @@ -16,33 +19,6 @@ class RerankingModelConfig(BaseModel): model: str -class VectorSetting(BaseModel): - """ - Vector Setting. - """ - - vector_weight: float - embedding_provider_name: str - embedding_model_name: str - - -class KeywordSetting(BaseModel): - """ - Keyword Setting. - """ - - keyword_weight: float - - -class WeightedScoreConfig(BaseModel): - """ - Weighted score Config. - """ - - vector_setting: VectorSetting - keyword_setting: KeywordSetting - - class MultipleRetrievalConfig(BaseModel): """ Multiple Retrieval Config. @@ -64,50 +40,6 @@ class SingleRetrievalConfig(BaseModel): model: ModelConfig -SupportedComparisonOperator = Literal[ - # for string or array - "contains", - "not contains", - "start with", - "end with", - "is", - "is not", - "empty", - "not empty", - "in", - "not in", - # for number - "=", - "≠", - ">", - "<", - "≥", - "≤", - # for time - "before", - "after", -] - - -class Condition(BaseModel): - """ - Condition detail - """ - - name: str - comparison_operator: SupportedComparisonOperator - value: str | Sequence[str] | None | int | float = None - - -class MetadataFilteringCondition(BaseModel): - """ - Metadata Filtering Condition. - """ - - logical_operator: Literal["and", "or"] | None = "and" - conditions: list[Condition] | None = Field(default=None, deprecated=True) - - class KnowledgeRetrievalNodeData(BaseNodeData): """ Knowledge retrieval Node Data. diff --git a/api/core/workflow/workflow_entry.py b/api/core/workflow/workflow_entry.py index 2346a95d6a..cecc20145a 100644 --- a/api/core/workflow/workflow_entry.py +++ b/api/core/workflow/workflow_entry.py @@ -1,7 +1,7 @@ import logging import time from collections.abc import Generator, Mapping, Sequence -from typing import Any +from typing import Any, TypedDict from graphon.entities import GraphInitParams from graphon.entities.graph_config import NodeConfigDictAdapter @@ -107,6 +107,26 @@ class _WorkflowChildEngineBuilder: return child_engine +class _NodeConfigDict(TypedDict): + id: str + width: int + height: int + type: str + data: dict[str, Any] + + +class _EdgeConfigDict(TypedDict): + source: str + target: str + sourceHandle: str + targetHandle: str + + +class SingleNodeGraphDict(TypedDict): + nodes: list[_NodeConfigDict] + edges: list[_EdgeConfigDict] + + class WorkflowEntry: def __init__( self, @@ -318,7 +338,7 @@ class WorkflowEntry: node_data: dict[str, Any], node_width: int = 114, node_height: int = 514, - ) -> dict[str, Any]: + ) -> SingleNodeGraphDict: """ Create a minimal graph structure for testing a single node in isolation. @@ -328,14 +348,14 @@ class WorkflowEntry: :param node_height: height for UI layout (default: 100) :return: graph dictionary with start node and target node """ - node_config = { + node_config: _NodeConfigDict = { "id": node_id, "width": node_width, "height": node_height, "type": "custom", "data": node_data, } - start_node_config = { + start_node_config: _NodeConfigDict = { "id": "start", "width": node_width, "height": node_height, @@ -346,9 +366,9 @@ class WorkflowEntry: "desc": "Start", }, } - return { - "nodes": [start_node_config, node_config], - "edges": [ + return SingleNodeGraphDict( + nodes=[start_node_config, node_config], + edges=[ { "source": "start", "target": node_id, @@ -356,7 +376,7 @@ class WorkflowEntry: "targetHandle": "target", } ], - } + ) @classmethod def run_free_node( diff --git a/api/dify_app.py b/api/dify_app.py index d6deb8e007..bbe3f33787 100644 --- a/api/dify_app.py +++ b/api/dify_app.py @@ -1,5 +1,14 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from flask import Flask +if TYPE_CHECKING: + from extensions.ext_login import DifyLoginManager + class DifyApp(Flask): - pass + """Flask application type with Dify-specific extension attributes.""" + + login_manager: DifyLoginManager diff --git a/api/events/event_handlers/sync_workflow_schedule_when_app_published.py b/api/events/event_handlers/sync_workflow_schedule_when_app_published.py index 168513fc04..5f8fcd8617 100644 --- a/api/events/event_handlers/sync_workflow_schedule_when_app_published.py +++ b/api/events/event_handlers/sync_workflow_schedule_when_app_published.py @@ -2,7 +2,7 @@ import logging from typing import cast from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from core.workflow.nodes.trigger_schedule.entities import SchedulePlanUpdate from events.app_event import app_published_workflow_was_updated @@ -45,7 +45,7 @@ def sync_schedule_from_workflow(tenant_id: str, app_id: str, workflow: Workflow) Returns: Updated or created WorkflowSchedulePlan, or None if no schedule node """ - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: schedule_config = ScheduleService.extract_schedule_config(workflow) existing_plan = session.scalar( @@ -59,7 +59,6 @@ def sync_schedule_from_workflow(tenant_id: str, app_id: str, workflow: Workflow) if existing_plan: logger.info("No schedule node in workflow for app %s, removing schedule plan", app_id) ScheduleService.delete_schedule(session=session, schedule_id=existing_plan.id) - session.commit() return None if existing_plan: @@ -73,7 +72,6 @@ def sync_schedule_from_workflow(tenant_id: str, app_id: str, workflow: Workflow) schedule_id=existing_plan.id, updates=updates, ) - session.commit() return updated_plan else: new_plan = ScheduleService.create_schedule( @@ -82,5 +80,4 @@ def sync_schedule_from_workflow(tenant_id: str, app_id: str, workflow: Workflow) app_id=app_id, config=schedule_config, ) - session.commit() return new_plan diff --git a/api/events/event_handlers/update_app_triggers_when_app_published_workflow_updated.py b/api/events/event_handlers/update_app_triggers_when_app_published_workflow_updated.py index b3917d5622..d55fe262fb 100644 --- a/api/events/event_handlers/update_app_triggers_when_app_published_workflow_updated.py +++ b/api/events/event_handlers/update_app_triggers_when_app_published_workflow_updated.py @@ -1,7 +1,7 @@ from typing import cast from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from core.trigger.constants import TRIGGER_NODE_TYPES from events.app_event import app_published_workflow_was_updated @@ -31,7 +31,7 @@ def handle(sender, **kwargs): # Extract trigger info from workflow trigger_infos = get_trigger_infos_from_workflow(published_workflow) - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: # Get existing app triggers existing_triggers = ( session.execute( @@ -79,8 +79,6 @@ def handle(sender, **kwargs): existing_trigger.title = new_title session.add(existing_trigger) - session.commit() - def get_trigger_infos_from_workflow(published_workflow: Workflow) -> list[dict]: """ diff --git a/api/events/event_handlers/update_provider_when_message_created.py b/api/events/event_handlers/update_provider_when_message_created.py index f68cdaadde..1d615f0f87 100644 --- a/api/events/event_handlers/update_provider_when_message_created.py +++ b/api/events/event_handlers/update_provider_when_message_created.py @@ -135,37 +135,40 @@ def handle(sender: Message, **kwargs): model_name=model_config.model, ) if used_quota is not None: - if provider_configuration.system_configuration.current_quota_type == ProviderQuotaType.TRIAL: - from services.credit_pool_service import CreditPoolService + match provider_configuration.system_configuration.current_quota_type: + case ProviderQuotaType.TRIAL: + from services.credit_pool_service import CreditPoolService - CreditPoolService.check_and_deduct_credits( - tenant_id=tenant_id, - credits_required=used_quota, - pool_type="trial", - ) - elif provider_configuration.system_configuration.current_quota_type == ProviderQuotaType.PAID: - from services.credit_pool_service import CreditPoolService - - CreditPoolService.check_and_deduct_credits( - tenant_id=tenant_id, - credits_required=used_quota, - pool_type="paid", - ) - else: - quota_update = _ProviderUpdateOperation( - filters=_ProviderUpdateFilters( + CreditPoolService.check_and_deduct_credits( tenant_id=tenant_id, - provider_name=ModelProviderID(model_config.provider).provider_name, - provider_type=ProviderType.SYSTEM.value, - quota_type=provider_configuration.system_configuration.current_quota_type, - ), - values=_ProviderUpdateValues(quota_used=Provider.quota_used + used_quota, last_used=current_time), - additional_filters=_ProviderUpdateAdditionalFilters( - quota_limit_check=True # Provider.quota_limit > Provider.quota_used - ), - description="quota_deduction_update", - ) - updates_to_perform.append(quota_update) + credits_required=used_quota, + pool_type="trial", + ) + case ProviderQuotaType.PAID: + from services.credit_pool_service import CreditPoolService + + CreditPoolService.check_and_deduct_credits( + tenant_id=tenant_id, + credits_required=used_quota, + pool_type="paid", + ) + case ProviderQuotaType.FREE: + quota_update = _ProviderUpdateOperation( + filters=_ProviderUpdateFilters( + tenant_id=tenant_id, + provider_name=ModelProviderID(model_config.provider).provider_name, + provider_type=ProviderType.SYSTEM.value, + quota_type=provider_configuration.system_configuration.current_quota_type, + ), + values=_ProviderUpdateValues( + quota_used=Provider.quota_used + used_quota, last_used=current_time + ), + additional_filters=_ProviderUpdateAdditionalFilters( + quota_limit_check=True # Provider.quota_limit > Provider.quota_used + ), + description="quota_deduction_update", + ) + updates_to_perform.append(quota_update) # Execute all updates start_time = time_module.perf_counter() diff --git a/api/extensions/ext_celery.py b/api/extensions/ext_celery.py index 4eed34436a..1b3ccd1207 100644 --- a/api/extensions/ext_celery.py +++ b/api/extensions/ext_celery.py @@ -10,7 +10,7 @@ from configs import dify_config from dify_app import DifyApp -def _get_celery_ssl_options() -> dict[str, Any] | None: +def get_celery_ssl_options() -> dict[str, Any] | None: """Get SSL configuration for Celery broker/backend connections.""" # Only apply SSL if we're using Redis as broker/backend if not dify_config.BROKER_USE_SSL: @@ -43,6 +43,19 @@ def _get_celery_ssl_options() -> dict[str, Any] | None: return ssl_options +def get_celery_broker_transport_options() -> dict[str, Any]: + """Get broker transport options (e.g. Redis Sentinel) for Celery connections.""" + if dify_config.CELERY_USE_SENTINEL: + return { + "master_name": dify_config.CELERY_SENTINEL_MASTER_NAME, + "sentinel_kwargs": { + "socket_timeout": dify_config.CELERY_SENTINEL_SOCKET_TIMEOUT, + "password": dify_config.CELERY_SENTINEL_PASSWORD, + }, + } + return {} + + def init_app(app: DifyApp) -> Celery: class FlaskTask(Task): def __call__(self, *args: object, **kwargs: object) -> object: @@ -53,16 +66,7 @@ def init_app(app: DifyApp) -> Celery: init_request_context() return self.run(*args, **kwargs) - broker_transport_options = {} - - if dify_config.CELERY_USE_SENTINEL: - broker_transport_options = { - "master_name": dify_config.CELERY_SENTINEL_MASTER_NAME, - "sentinel_kwargs": { - "socket_timeout": dify_config.CELERY_SENTINEL_SOCKET_TIMEOUT, - "password": dify_config.CELERY_SENTINEL_PASSWORD, - }, - } + broker_transport_options = get_celery_broker_transport_options() celery_app = Celery( app.name, @@ -89,7 +93,7 @@ def init_app(app: DifyApp) -> Celery: ) # Apply SSL configuration if enabled - ssl_options = _get_celery_ssl_options() + ssl_options = get_celery_ssl_options() if ssl_options: celery_app.conf.update( broker_use_ssl=ssl_options, diff --git a/api/extensions/ext_login.py b/api/extensions/ext_login.py index 02e50a90fc..bc59eaca63 100644 --- a/api/extensions/ext_login.py +++ b/api/extensions/ext_login.py @@ -1,7 +1,8 @@ import json +from typing import cast import flask_login -from flask import Response, request +from flask import Request, Response, request from flask_login import user_loaded_from_request, user_logged_in from sqlalchemy import select from werkzeug.exceptions import NotFound, Unauthorized @@ -16,13 +17,35 @@ from models import Account, Tenant, TenantAccountJoin from models.model import AppMCPServer, EndUser from services.account_service import AccountService -login_manager = flask_login.LoginManager() +type LoginUser = Account | EndUser + + +class DifyLoginManager(flask_login.LoginManager): + """Project-specific Flask-Login manager with a stable unauthorized contract. + + Dify registers `unauthorized_handler` below to always return a JSON `Response`. + Overriding this method lets callers rely on that narrower return type instead of + Flask-Login's broader callback contract. + """ + + def unauthorized(self) -> Response: + """Return the registered unauthorized handler result as a Flask `Response`.""" + return cast(Response, super().unauthorized()) + + def load_user_from_request_context(self) -> None: + """Populate Flask-Login's request-local user cache for the current request.""" + self._load_user() + + +login_manager = DifyLoginManager() # Flask-Login configuration @login_manager.request_loader -def load_user_from_request(request_from_flask_login): +def load_user_from_request(request_from_flask_login: Request) -> LoginUser | None: """Load user based on the request.""" + del request_from_flask_login + # Skip authentication for documentation endpoints if dify_config.SWAGGER_UI_ENABLED and request.path.endswith((dify_config.SWAGGER_UI_PATH, "/swagger.json")): return None @@ -100,10 +123,12 @@ def load_user_from_request(request_from_flask_login): raise NotFound("End user not found.") return end_user + return None + @user_logged_in.connect @user_loaded_from_request.connect -def on_user_logged_in(_sender, user): +def on_user_logged_in(_sender: object, user: LoginUser) -> None: """Called when a user logged in. Note: AccountService.load_logged_in_account will populate user.current_tenant_id @@ -114,8 +139,10 @@ def on_user_logged_in(_sender, user): @login_manager.unauthorized_handler -def unauthorized_handler(): +def unauthorized_handler() -> Response: """Handle unauthorized requests.""" + # Keep this as a concrete `Response`; `DifyLoginManager.unauthorized()` narrows + # Flask-Login's callback contract based on this override. return Response( json.dumps({"code": "unauthorized", "message": "Unauthorized."}), status=401, @@ -123,5 +150,5 @@ def unauthorized_handler(): ) -def init_app(app: DifyApp): +def init_app(app: DifyApp) -> None: login_manager.init_app(app) diff --git a/api/extensions/logstore/repositories/logstore_api_workflow_run_repository.py b/api/extensions/logstore/repositories/logstore_api_workflow_run_repository.py index 3c83ab4f84..2745141431 100644 --- a/api/extensions/logstore/repositories/logstore_api_workflow_run_repository.py +++ b/api/extensions/logstore/repositories/logstore_api_workflow_run_repository.py @@ -354,11 +354,11 @@ class LogstoreAPIWorkflowRunRepository(APIWorkflowRunRepository): ) -> WorkflowRun | None: """Fallback to PostgreSQL query for records not in LogStore (with tenant isolation).""" from sqlalchemy import select - from sqlalchemy.orm import Session + from sqlalchemy.orm import sessionmaker from extensions.ext_database import db - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: stmt = select(WorkflowRun).where( WorkflowRun.id == run_id, WorkflowRun.tenant_id == tenant_id, WorkflowRun.app_id == app_id ) @@ -439,11 +439,11 @@ class LogstoreAPIWorkflowRunRepository(APIWorkflowRunRepository): def _fallback_get_workflow_run_by_id(self, run_id: str) -> WorkflowRun | None: """Fallback to PostgreSQL query for records not in LogStore.""" from sqlalchemy import select - from sqlalchemy.orm import Session + from sqlalchemy.orm import sessionmaker from extensions.ext_database import db - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: stmt = select(WorkflowRun).where(WorkflowRun.id == run_id) return session.scalar(stmt) diff --git a/api/extensions/otel/celery_sqlcommenter.py b/api/extensions/otel/celery_sqlcommenter.py index 8abb1ce15a..15e52fb5ef 100644 --- a/api/extensions/otel/celery_sqlcommenter.py +++ b/api/extensions/otel/celery_sqlcommenter.py @@ -11,7 +11,7 @@ SQLAlchemy instrumentor appends comments to SQL statements. """ import logging -from typing import Any +from typing import Any, TypedDict from celery.signals import task_postrun, task_prerun from opentelemetry import context @@ -24,9 +24,17 @@ _SQLCOMMENTER_CONTEXT_KEY = "SQLCOMMENTER_ORM_TAGS_AND_VALUES" _TOKEN_ATTR = "_dify_sqlcommenter_context_token" -def _build_celery_sqlcommenter_tags(task: Any) -> dict[str, str | int]: +class CelerySqlcommenterTagsDict(TypedDict, total=False): + framework: str + task_name: str + traceparent: str + celery_retries: int + routing_key: str + + +def _build_celery_sqlcommenter_tags(task: Any) -> CelerySqlcommenterTagsDict: """Build SQL commenter tags from the current Celery task and OpenTelemetry context.""" - tags: dict[str, str | int] = {} + tags: CelerySqlcommenterTagsDict = {} try: tags["framework"] = f"celery:{_get_celery_version()}" diff --git a/api/extensions/storage/clickzetta_volume/file_lifecycle.py b/api/extensions/storage/clickzetta_volume/file_lifecycle.py index 483bd6bbf6..86b1bba544 100644 --- a/api/extensions/storage/clickzetta_volume/file_lifecycle.py +++ b/api/extensions/storage/clickzetta_volume/file_lifecycle.py @@ -13,7 +13,7 @@ import operator from dataclasses import asdict, dataclass from datetime import datetime from enum import StrEnum, auto -from typing import Any +from typing import Any, TypedDict from pydantic import TypeAdapter @@ -22,6 +22,17 @@ logger = logging.getLogger(__name__) _metadata_adapter: TypeAdapter[dict[str, Any]] = TypeAdapter(dict[str, Any]) +class StorageStatisticsDict(TypedDict): + total_files: int + active_files: int + archived_files: int + deleted_files: int + total_size: int + versions_count: int + oldest_file: str | None + newest_file: str | None + + class FileStatus(StrEnum): """File status enumeration""" @@ -384,7 +395,7 @@ class FileLifecycleManager: logger.exception("Failed to cleanup old versions") return 0 - def get_storage_statistics(self) -> dict[str, Any]: + def get_storage_statistics(self) -> StorageStatisticsDict: """Get storage statistics Returns: @@ -393,16 +404,16 @@ class FileLifecycleManager: try: metadata_dict = self._load_metadata() - stats: dict[str, Any] = { - "total_files": len(metadata_dict), - "active_files": 0, - "archived_files": 0, - "deleted_files": 0, - "total_size": 0, - "versions_count": 0, - "oldest_file": None, - "newest_file": None, - } + stats = StorageStatisticsDict( + total_files=len(metadata_dict), + active_files=0, + archived_files=0, + deleted_files=0, + total_size=0, + versions_count=0, + oldest_file=None, + newest_file=None, + ) oldest_date = None newest_date = None @@ -437,7 +448,16 @@ class FileLifecycleManager: except Exception: logger.exception("Failed to get storage statistics") - return {} + return StorageStatisticsDict( + total_files=0, + active_files=0, + archived_files=0, + deleted_files=0, + total_size=0, + versions_count=0, + oldest_file=None, + newest_file=None, + ) def _create_version_backup(self, filename: str, metadata: dict): """Create version backup""" diff --git a/api/fields/annotation_fields.py b/api/fields/annotation_fields.py index a646950722..b2a0e92c47 100644 --- a/api/fields/annotation_fields.py +++ b/api/fields/annotation_fields.py @@ -2,7 +2,9 @@ from __future__ import annotations from datetime import datetime -from pydantic import BaseModel, ConfigDict, Field, field_validator +from pydantic import Field, field_validator + +from fields.base import ResponseModel def _to_timestamp(value: datetime | int | None) -> int | None: @@ -11,16 +13,6 @@ def _to_timestamp(value: datetime | int | None) -> int | None: return value -class ResponseModel(BaseModel): - model_config = ConfigDict( - from_attributes=True, - extra="ignore", - populate_by_name=True, - serialize_by_alias=True, - protected_namespaces=(), - ) - - class Annotation(ResponseModel): id: str question: str | None = None diff --git a/api/fields/base.py b/api/fields/base.py new file mode 100644 index 0000000000..b806ab6c9c --- /dev/null +++ b/api/fields/base.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from pydantic import BaseModel, ConfigDict + + +class ResponseModel(BaseModel): + model_config = ConfigDict( + from_attributes=True, + extra="ignore", + populate_by_name=True, + serialize_by_alias=True, + protected_namespaces=(), + ) diff --git a/api/fields/conversation_fields.py b/api/fields/conversation_fields.py index b1d1b4caac..7878d58679 100644 --- a/api/fields/conversation_fields.py +++ b/api/fields/conversation_fields.py @@ -4,21 +4,13 @@ from datetime import datetime from typing import Any from graphon.file import File -from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator +from pydantic import Field, field_validator, model_validator + +from fields.base import ResponseModel type JSONValue = Any -class ResponseModel(BaseModel): - model_config = ConfigDict( - from_attributes=True, - extra="ignore", - populate_by_name=True, - serialize_by_alias=True, - protected_namespaces=(), - ) - - class MessageFile(ResponseModel): id: str filename: str diff --git a/api/fields/end_user_fields.py b/api/fields/end_user_fields.py index df1980616a..3851933cc2 100644 --- a/api/fields/end_user_fields.py +++ b/api/fields/end_user_fields.py @@ -3,7 +3,9 @@ from __future__ import annotations from datetime import datetime from flask_restx import fields -from pydantic import BaseModel, ConfigDict, Field +from pydantic import Field + +from fields.base import ResponseModel simple_end_user_fields = { "id": fields.String, @@ -26,16 +28,6 @@ end_user_detail_fields = { } -class ResponseModel(BaseModel): - model_config = ConfigDict( - from_attributes=True, - extra="ignore", - populate_by_name=True, - serialize_by_alias=True, - protected_namespaces=(), - ) - - class SimpleEndUser(ResponseModel): id: str type: str diff --git a/api/fields/file_fields.py b/api/fields/file_fields.py index 913fb675f9..ad8b95e4dc 100644 --- a/api/fields/file_fields.py +++ b/api/fields/file_fields.py @@ -2,17 +2,9 @@ from __future__ import annotations from datetime import datetime -from pydantic import BaseModel, ConfigDict, field_validator +from pydantic import field_validator - -class ResponseModel(BaseModel): - model_config = ConfigDict( - from_attributes=True, - extra="ignore", - populate_by_name=True, - serialize_by_alias=True, - protected_namespaces=(), - ) +from fields.base import ResponseModel def _to_timestamp(value: datetime | int | None) -> int | None: diff --git a/api/fields/member_fields.py b/api/fields/member_fields.py index b8daa5af30..cfe0015918 100644 --- a/api/fields/member_fields.py +++ b/api/fields/member_fields.py @@ -4,7 +4,9 @@ from datetime import datetime from flask_restx import fields from graphon.file import helpers as file_helpers -from pydantic import BaseModel, ConfigDict, computed_field, field_validator +from pydantic import computed_field, field_validator + +from fields.base import ResponseModel simple_account_fields = { "id": fields.String, @@ -27,16 +29,6 @@ def _build_avatar_url(avatar: str | None) -> str | None: return file_helpers.get_signed_file_url(avatar) -class ResponseModel(BaseModel): - model_config = ConfigDict( - from_attributes=True, - extra="ignore", - populate_by_name=True, - serialize_by_alias=True, - protected_namespaces=(), - ) - - class SimpleAccount(ResponseModel): id: str name: str diff --git a/api/fields/tag_fields.py b/api/fields/tag_fields.py index 7cb64e5ca8..a3629f477a 100644 --- a/api/fields/tag_fields.py +++ b/api/fields/tag_fields.py @@ -1,16 +1,6 @@ from __future__ import annotations -from pydantic import BaseModel, ConfigDict - - -class ResponseModel(BaseModel): - model_config = ConfigDict( - from_attributes=True, - extra="ignore", - populate_by_name=True, - serialize_by_alias=True, - protected_namespaces=(), - ) +from fields.base import ResponseModel class DataSetTag(ResponseModel): diff --git a/api/libs/helper.py b/api/libs/helper.py index a7b3da77ff..ece53e8806 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -18,8 +18,9 @@ from flask import Response, stream_with_context from flask_restx import fields from graphon.file import helpers as file_helpers from graphon.model_runtime.utils.encoders import jsonable_encoder -from pydantic import BaseModel +from pydantic import BaseModel, TypeAdapter from pydantic.functional_validators import AfterValidator +from typing_extensions import TypedDict from configs import dify_config from core.app.features.rate_limiting.rate_limit import RateLimitGenerator @@ -32,6 +33,17 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) +class _TokenData(TypedDict, total=False): + account_id: str | None + email: str + token_type: str + code: str + old_email: str + + +_token_data_adapter: TypeAdapter[_TokenData] = TypeAdapter(_TokenData) + + def _stream_with_request_context(response: object) -> Any: """Bridge Flask's loosely-typed streaming helper without leaking casts into callers.""" return cast(Any, stream_with_context)(response) @@ -443,7 +455,7 @@ class TokenManager: if token_data_json is None: logger.warning("%s token %s not found with key %s", token_type, token, key) return None - token_data: dict[str, Any] | None = json.loads(token_data_json) + token_data = dict(_token_data_adapter.validate_json(token_data_json)) return token_data @classmethod diff --git a/api/libs/login.py b/api/libs/login.py index 68a2050747..067597cb3c 100644 --- a/api/libs/login.py +++ b/api/libs/login.py @@ -2,19 +2,19 @@ from __future__ import annotations from collections.abc import Callable from functools import wraps -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast -from flask import current_app, g, has_request_context, request +from flask import Response, current_app, g, has_request_context, request from flask_login.config import EXEMPT_METHODS from werkzeug.local import LocalProxy from configs import dify_config +from dify_app import DifyApp +from extensions.ext_login import DifyLoginManager from libs.token import check_csrf_token from models import Account if TYPE_CHECKING: - from flask.typing import ResponseReturnValue - from models.model import EndUser @@ -29,7 +29,13 @@ def _resolve_current_user() -> EndUser | Account | None: return get_current_object() if callable(get_current_object) else user_proxy # type: ignore -def current_account_with_tenant(): +def _get_login_manager() -> DifyLoginManager: + """Return the project login manager with Dify's narrowed unauthorized contract.""" + app = cast(DifyApp, current_app) + return app.login_manager + + +def current_account_with_tenant() -> tuple[Account, str]: """ Resolve the underlying account for the current user proxy and ensure tenant context exists. Allows tests to supply plain Account mocks without the LocalProxy helper. @@ -42,7 +48,7 @@ def current_account_with_tenant(): return user, user.current_tenant_id -def login_required[**P, R](func: Callable[P, R]) -> Callable[P, R | ResponseReturnValue]: +def login_required[**P, R](func: Callable[P, R]) -> Callable[P, R | Response]: """ If you decorate a view with this, it will ensure that the current user is logged in and authenticated before calling the actual view. (If they are @@ -77,13 +83,16 @@ def login_required[**P, R](func: Callable[P, R]) -> Callable[P, R | ResponseRetu """ @wraps(func) - def decorated_view(*args: P.args, **kwargs: P.kwargs) -> R | ResponseReturnValue: + def decorated_view(*args: P.args, **kwargs: P.kwargs) -> R | Response: if request.method in EXEMPT_METHODS or dify_config.LOGIN_DISABLED: return current_app.ensure_sync(func)(*args, **kwargs) user = _resolve_current_user() if user is None or not user.is_authenticated: - return current_app.login_manager.unauthorized() # type: ignore + # `DifyLoginManager` guarantees that the registered unauthorized handler + # is surfaced here as a concrete Flask `Response`. + unauthorized_response: Response = _get_login_manager().unauthorized() + return unauthorized_response g._login_user = user # we put csrf validation here for less conflicts # TODO: maybe find a better place for it. @@ -96,7 +105,7 @@ def login_required[**P, R](func: Callable[P, R]) -> Callable[P, R | ResponseRetu def _get_user() -> EndUser | Account | None: if has_request_context(): if "_login_user" not in g: - current_app.login_manager._load_user() # type: ignore + _get_login_manager().load_user_from_request_context() return g._login_user diff --git a/api/models/account.py b/api/models/account.py index 5960ac6564..a3074c6f63 100644 --- a/api/models/account.py +++ b/api/models/account.py @@ -2,7 +2,7 @@ import enum import json from dataclasses import field from datetime import datetime -from typing import Any, Optional +from typing import Optional, TypedDict from uuid import uuid4 import sqlalchemy as sa @@ -232,6 +232,11 @@ class TenantStatus(enum.StrEnum): ARCHIVE = "archive" +class TenantCustomConfigDict(TypedDict, total=False): + remove_webapp_brand: bool + replace_webapp_logo: str | None + + class Tenant(TypeBase): __tablename__ = "tenants" __table_args__ = (sa.PrimaryKeyConstraint("id", name="tenant_pkey"),) @@ -263,11 +268,11 @@ class Tenant(TypeBase): ) @property - def custom_config_dict(self) -> dict[str, Any]: + def custom_config_dict(self) -> TenantCustomConfigDict: return json.loads(self.custom_config) if self.custom_config else {} @custom_config_dict.setter - def custom_config_dict(self, value: dict[str, Any]) -> None: + def custom_config_dict(self, value: TenantCustomConfigDict) -> None: self.custom_config = json.dumps(value) diff --git a/api/models/dataset.py b/api/models/dataset.py index e323ccfd7f..97604848af 100644 --- a/api/models/dataset.py +++ b/api/models/dataset.py @@ -19,6 +19,7 @@ from sqlalchemy import DateTime, String, func, select from sqlalchemy.orm import Mapped, Session, mapped_column from configs import dify_config +from core.rag.entities import ParentMode, Rule from core.rag.index_processor.constant.built_in_field import BuiltInField, MetadataDataSource from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType from core.rag.index_processor.constant.query_type import QueryType @@ -26,7 +27,6 @@ from core.rag.retrieval.retrieval_methods import RetrievalMethod from core.tools.signature import sign_upload_file from extensions.ext_storage import storage from libs.uuid_utils import uuidv7 -from services.entities.knowledge_entities.knowledge_entities import ParentMode, Rule from .account import Account from .base import Base, TypeBase diff --git a/api/models/model.py b/api/models/model.py index 1d73aadf09..43ddf344d2 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -18,7 +18,7 @@ from graphon.enums import WorkflowExecutionStatus from graphon.file import FILE_MODEL_IDENTITY, File, FileTransferMethod, FileType from graphon.file import helpers as file_helpers from sqlalchemy import BigInteger, Float, Index, PrimaryKeyConstraint, String, exists, func, select, text -from sqlalchemy.orm import Mapped, Session, mapped_column +from sqlalchemy.orm import Mapped, Session, mapped_column, sessionmaker from configs import dify_config from constants import DEFAULT_FILE_NUMBER_LIMITS @@ -524,7 +524,7 @@ class App(Base): if not api_provider_ids and not builtin_provider_ids: return [] - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: if api_provider_ids: existing_api_providers = [ str(api_provider.id) diff --git a/api/models/tools.py b/api/models/tools.py index d8731fb8a8..02f8b5217d 100644 --- a/api/models/tools.py +++ b/api/models/tools.py @@ -356,7 +356,7 @@ class MCPToolProvider(TypeBase): return {} @property - def headers(self) -> dict[str, Any]: + def headers(self) -> dict[str, str]: if self.encrypted_headers is None: return {} try: diff --git a/api/models/types.py b/api/models/types.py index 9ab694759f..c1d9c3845a 100644 --- a/api/models/types.py +++ b/api/models/types.py @@ -1,6 +1,6 @@ import enum import uuid -from typing import Any +from typing import Any, cast import sqlalchemy as sa from sqlalchemy import CHAR, TEXT, VARCHAR, LargeBinary, TypeDecorator @@ -143,8 +143,14 @@ class EnumText[T: enum.StrEnum](TypeDecorator[T | None]): def process_result_value(self, value: str | None, dialect: Dialect) -> T | None: if value is None or value == "": return None - # Type annotation guarantees value is str at this point - return self._enum_class(value) + try: + # Type annotation guarantees value is str at this point + return self._enum_class(value) + except ValueError: + value_of = getattr(self._enum_class, "value_of", None) + if callable(value_of): + return cast(T, value_of(value)) + raise def compare_values(self, x: T | None, y: T | None) -> bool: if x is None or y is None: diff --git a/api/pyproject.toml b/api/pyproject.toml index 863b61cad1..dab420fc87 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -8,13 +8,13 @@ dependencies = [ "arize-phoenix-otel~=0.15.0", "azure-identity==1.25.3", "beautifulsoup4==4.14.3", - "boto3==1.42.78", + "boto3==1.42.83", "bs4~=0.0.1", "cachetools~=5.3.0", "celery~=5.6.2", "charset-normalizer>=3.4.4", "flask~=3.1.2", - "flask-compress>=1.17,<1.24", + "flask-compress>=1.17,<1.25", "flask-cors~=6.0.0", "flask-login~=0.6.3", "flask-migrate~=4.1.0", @@ -25,7 +25,7 @@ dependencies = [ "google-api-core>=2.19.1", "google-api-python-client==2.193.0", "google-auth>=2.47.0", - "google-auth-httplib2==0.3.0", + "google-auth-httplib2==0.3.1", "google-cloud-aiplatform>=1.123.0", "googleapis-common-protos>=1.65.0", "graphon>=0.1.2", @@ -111,9 +111,9 @@ package = false dev = [ "coverage~=7.13.4", "dotenv-linter~=0.7.0", - "faker~=40.11.0", + "faker~=40.12.0", "lxml-stubs~=0.5.1", - "basedpyright~=1.38.2", + "basedpyright~=1.39.0", "ruff~=0.15.5", "pytest~=9.0.2", "pytest-benchmark~=5.2.3", @@ -139,15 +139,15 @@ dev = [ "types-olefile~=0.47.0", "types-openpyxl~=3.1.5", "types-pexpect~=4.9.0", - "types-protobuf~=6.32.1", + "types-protobuf~=7.34.1", "types-psutil~=7.2.2", "types-psycopg2~=2.9.21", - "types-pygments~=2.19.0", + "types-pygments~=2.20.0", "types-pymysql~=1.1.0", "types-python-dateutil~=2.9.0", "types-pywin32~=311.0.0", "types-pyyaml~=6.0.12", - "types-regex~=2026.3.32", + "types-regex~=2026.4.4", "types-shapely~=2.1.0", "types-simplejson>=3.20.0", "types-six>=1.17.0", @@ -166,12 +166,12 @@ dev = [ "import-linter>=2.3", "types-redis>=4.6.0.20241004", "celery-types>=0.23.0", - "mypy~=1.19.1", + "mypy~=1.20.0", # "locust>=2.40.4", # Temporarily removed due to compatibility issues. Uncomment when resolved. "sseclient-py>=1.8.0", "pytest-timeout>=2.4.0", "pytest-xdist>=3.8.0", - "pyrefly>=0.57.1", + "pyrefly>=0.59.1", ] ############################################################ @@ -200,23 +200,23 @@ tools = ["cloudscraper~=1.2.71", "nltk~=3.9.1"] # Required by vector store clients ############################################################ vdb = [ - "alibabacloud_gpdb20160503~=5.1.0", + "alibabacloud_gpdb20160503~=5.2.0", "alibabacloud_tea_openapi~=0.4.3", "chromadb==0.5.20", "clickhouse-connect~=0.15.0", "clickzetta-connector-python>=0.8.102", - "couchbase~=4.5.0", + "couchbase~=4.6.0", "elasticsearch==8.14.0", "opensearch-py==3.1.0", "oracledb==3.4.2", "pgvecto-rs[sqlalchemy]~=0.2.1", "pgvector==0.4.2", "pymilvus~=2.6.10", - "pymochow==2.3.6", + "pymochow==2.4.0", "pyobvector~=0.2.17", "qdrant-client==1.9.0", "intersystems-irispython>=5.1.0", - "tablestore==6.4.2", + "tablestore==6.4.3", "tcvectordb~=2.1.0", "tidb-vector==0.0.15", "upstash-vector==0.8.0", diff --git a/api/repositories/api_workflow_run_repository.py b/api/repositories/api_workflow_run_repository.py index 1a2a539c80..100589804c 100644 --- a/api/repositories/api_workflow_run_repository.py +++ b/api/repositories/api_workflow_run_repository.py @@ -36,7 +36,7 @@ Example: from collections.abc import Callable, Sequence from datetime import datetime -from typing import Protocol +from typing import Protocol, TypedDict from graphon.entities.pause_reason import PauseReason from graphon.enums import WorkflowType @@ -55,6 +55,16 @@ from repositories.types import ( ) +class RunsWithRelatedCountsDict(TypedDict): + runs: int + node_executions: int + offloads: int + app_logs: int + trigger_logs: int + pauses: int + pause_reasons: int + + class APIWorkflowRunRepository(WorkflowExecutionRepository, Protocol): """ Protocol for service-layer WorkflowRun repository operations. @@ -333,7 +343,7 @@ class APIWorkflowRunRepository(WorkflowExecutionRepository, Protocol): runs: Sequence[WorkflowRun], delete_node_executions: Callable[[Session, Sequence[WorkflowRun]], tuple[int, int]] | None = None, delete_trigger_logs: Callable[[Session, Sequence[str]], int] | None = None, - ) -> dict[str, int]: + ) -> RunsWithRelatedCountsDict: """ Delete workflow runs and their related records (node executions, offloads, app logs, trigger logs, pauses, pause reasons). @@ -400,7 +410,7 @@ class APIWorkflowRunRepository(WorkflowExecutionRepository, Protocol): runs: Sequence[WorkflowRun], count_node_executions: Callable[[Session, Sequence[WorkflowRun]], tuple[int, int]] | None = None, count_trigger_logs: Callable[[Session, Sequence[str]], int] | None = None, - ) -> dict[str, int]: + ) -> RunsWithRelatedCountsDict: """ Count workflow runs and their related records (node executions, offloads, app logs, trigger logs, pauses, pause reasons) without deleting data. diff --git a/api/repositories/sqlalchemy_api_workflow_run_repository.py b/api/repositories/sqlalchemy_api_workflow_run_repository.py index 413936b542..9267be2636 100644 --- a/api/repositories/sqlalchemy_api_workflow_run_repository.py +++ b/api/repositories/sqlalchemy_api_workflow_run_repository.py @@ -45,7 +45,7 @@ from libs.uuid_utils import uuidv7 from models.enums import WorkflowRunTriggeredFrom from models.human_input import HumanInputForm from models.workflow import WorkflowAppLog, WorkflowArchiveLog, WorkflowPause, WorkflowPauseReason, WorkflowRun -from repositories.api_workflow_run_repository import APIWorkflowRunRepository +from repositories.api_workflow_run_repository import APIWorkflowRunRepository, RunsWithRelatedCountsDict from repositories.entities.workflow_pause import WorkflowPauseEntity from repositories.types import ( AverageInteractionStats, @@ -463,7 +463,7 @@ class DifyAPISQLAlchemyWorkflowRunRepository(APIWorkflowRunRepository): runs: Sequence[WorkflowRun], delete_node_executions: Callable[[Session, Sequence[WorkflowRun]], tuple[int, int]] | None = None, delete_trigger_logs: Callable[[Session, Sequence[str]], int] | None = None, - ) -> dict[str, int]: + ) -> RunsWithRelatedCountsDict: if not runs: return { "runs": 0, @@ -638,7 +638,7 @@ class DifyAPISQLAlchemyWorkflowRunRepository(APIWorkflowRunRepository): runs: Sequence[WorkflowRun], count_node_executions: Callable[[Session, Sequence[WorkflowRun]], tuple[int, int]] | None = None, count_trigger_logs: Callable[[Session, Sequence[str]], int] | None = None, - ) -> dict[str, int]: + ) -> RunsWithRelatedCountsDict: if not runs: return { "runs": 0, diff --git a/api/services/account_service.py b/api/services/account_service.py index 29b1444730..4b58b3b697 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -8,8 +8,8 @@ from hashlib import sha256 from typing import Any, TypedDict, cast from pydantic import BaseModel, TypeAdapter -from sqlalchemy import func, select -from sqlalchemy.orm import Session +from sqlalchemy import delete, func, select, update +from sqlalchemy.orm import Session, sessionmaker class InvitationData(TypedDict): @@ -83,6 +83,12 @@ from tasks.mail_reset_password_task import ( logger = logging.getLogger(__name__) +class InvitationDetailDict(TypedDict): + account: Account + data: InvitationData + tenant: Tenant + + def _try_join_enterprise_default_workspace(account_id: str) -> None: """Best-effort join to enterprise default workspace.""" if not dify_config.ENTERPRISE_ENABLED: @@ -144,22 +150,26 @@ class AccountService: @staticmethod def load_user(user_id: str) -> None | Account: - account = db.session.query(Account).filter_by(id=user_id).first() + account = db.session.get(Account, user_id) if not account: return None if account.status == AccountStatus.BANNED: raise Unauthorized("Account is banned.") - current_tenant = db.session.query(TenantAccountJoin).filter_by(account_id=account.id, current=True).first() + current_tenant = db.session.scalar( + select(TenantAccountJoin) + .where(TenantAccountJoin.account_id == account.id, TenantAccountJoin.current == True) + .limit(1) + ) if current_tenant: account.set_tenant_id(current_tenant.tenant_id) else: - available_ta = ( - db.session.query(TenantAccountJoin) - .filter_by(account_id=account.id) + available_ta = db.session.scalar( + select(TenantAccountJoin) + .where(TenantAccountJoin.account_id == account.id) .order_by(TenantAccountJoin.id.asc()) - .first() + .limit(1) ) if not available_ta: return None @@ -195,7 +205,7 @@ class AccountService: def authenticate(email: str, password: str, invite_token: str | None = None) -> Account: """authenticate account with email and password""" - account = db.session.query(Account).filter_by(email=email).first() + account = db.session.scalar(select(Account).where(Account.email == email).limit(1)) if not account: raise AccountPasswordError("Invalid email or password.") @@ -371,8 +381,10 @@ class AccountService: """Link account integrate""" try: # Query whether there is an existing binding record for the same provider - account_integrate: AccountIntegrate | None = ( - db.session.query(AccountIntegrate).filter_by(account_id=account.id, provider=provider).first() + account_integrate: AccountIntegrate | None = db.session.scalar( + select(AccountIntegrate) + .where(AccountIntegrate.account_id == account.id, AccountIntegrate.provider == provider) + .limit(1) ) if account_integrate: @@ -416,7 +428,9 @@ class AccountService: def update_account_email(account: Account, email: str) -> Account: """Update account email""" account.email = email - account_integrate = db.session.query(AccountIntegrate).filter_by(account_id=account.id).first() + account_integrate = db.session.scalar( + select(AccountIntegrate).where(AccountIntegrate.account_id == account.id).limit(1) + ) if account_integrate: db.session.delete(account_integrate) db.session.add(account) @@ -818,7 +832,7 @@ class AccountService: ) ) - account = db.session.query(Account).where(Account.email == email).first() + account = db.session.scalar(select(Account).where(Account.email == email).limit(1)) if not account: return None @@ -1018,7 +1032,7 @@ class AccountService: @staticmethod def check_email_unique(email: str) -> bool: - return db.session.query(Account).filter_by(email=email).first() is None + return db.session.scalar(select(Account).where(Account.email == email).limit(1)) is None class TenantService: @@ -1061,11 +1075,11 @@ class TenantService: @staticmethod def create_owner_tenant_if_not_exist(account: Account, name: str | None = None, is_setup: bool | None = False): """Check if user have a workspace or not""" - available_ta = ( - db.session.query(TenantAccountJoin) - .filter_by(account_id=account.id) + available_ta = db.session.scalar( + select(TenantAccountJoin) + .where(TenantAccountJoin.account_id == account.id) .order_by(TenantAccountJoin.id.asc()) - .first() + .limit(1) ) if available_ta: @@ -1096,7 +1110,11 @@ class TenantService: logger.error("Tenant %s has already an owner.", tenant.id) raise Exception("Tenant already has an owner.") - ta = db.session.query(TenantAccountJoin).filter_by(tenant_id=tenant.id, account_id=account.id).first() + ta = db.session.scalar( + select(TenantAccountJoin) + .where(TenantAccountJoin.tenant_id == tenant.id, TenantAccountJoin.account_id == account.id) + .limit(1) + ) if ta: ta.role = TenantAccountRole(role) else: @@ -1111,11 +1129,12 @@ class TenantService: @staticmethod def get_join_tenants(account: Account) -> list[Tenant]: """Get account join tenants""" - return ( - db.session.query(Tenant) - .join(TenantAccountJoin, Tenant.id == TenantAccountJoin.tenant_id) - .where(TenantAccountJoin.account_id == account.id, Tenant.status == TenantStatus.NORMAL) - .all() + return list( + db.session.scalars( + select(Tenant) + .join(TenantAccountJoin, Tenant.id == TenantAccountJoin.tenant_id) + .where(TenantAccountJoin.account_id == account.id, Tenant.status == TenantStatus.NORMAL) + ).all() ) @staticmethod @@ -1125,7 +1144,11 @@ class TenantService: if not tenant: raise TenantNotFoundError("Tenant not found.") - ta = db.session.query(TenantAccountJoin).filter_by(tenant_id=tenant.id, account_id=account.id).first() + ta = db.session.scalar( + select(TenantAccountJoin) + .where(TenantAccountJoin.tenant_id == tenant.id, TenantAccountJoin.account_id == account.id) + .limit(1) + ) if ta: tenant.role = ta.role else: @@ -1140,23 +1163,25 @@ class TenantService: if tenant_id is None: raise ValueError("Tenant ID must be provided.") - tenant_account_join = ( - db.session.query(TenantAccountJoin) + tenant_account_join = db.session.scalar( + select(TenantAccountJoin) .join(Tenant, TenantAccountJoin.tenant_id == Tenant.id) .where( TenantAccountJoin.account_id == account.id, TenantAccountJoin.tenant_id == tenant_id, Tenant.status == TenantStatus.NORMAL, ) - .first() + .limit(1) ) if not tenant_account_join: raise AccountNotLinkTenantError("Tenant not found or account is not a member of the tenant.") else: - db.session.query(TenantAccountJoin).where( - TenantAccountJoin.account_id == account.id, TenantAccountJoin.tenant_id != tenant_id - ).update({"current": False}) + db.session.execute( + update(TenantAccountJoin) + .where(TenantAccountJoin.account_id == account.id, TenantAccountJoin.tenant_id != tenant_id) + .values(current=False) + ) tenant_account_join.current = True # Set the current tenant for the account account.set_tenant_id(tenant_account_join.tenant_id) @@ -1165,8 +1190,8 @@ class TenantService: @staticmethod def get_tenant_members(tenant: Tenant) -> list[Account]: """Get tenant members""" - query = ( - db.session.query(Account, TenantAccountJoin.role) + stmt = ( + select(Account, TenantAccountJoin.role) .select_from(Account) .join(TenantAccountJoin, Account.id == TenantAccountJoin.account_id) .where(TenantAccountJoin.tenant_id == tenant.id) @@ -1175,7 +1200,7 @@ class TenantService: # Initialize an empty list to store the updated accounts updated_accounts = [] - for account, role in query: + for account, role in db.session.execute(stmt): account.role = role updated_accounts.append(account) @@ -1184,8 +1209,8 @@ class TenantService: @staticmethod def get_dataset_operator_members(tenant: Tenant) -> list[Account]: """Get dataset admin members""" - query = ( - db.session.query(Account, TenantAccountJoin.role) + stmt = ( + select(Account, TenantAccountJoin.role) .select_from(Account) .join(TenantAccountJoin, Account.id == TenantAccountJoin.account_id) .where(TenantAccountJoin.tenant_id == tenant.id) @@ -1195,7 +1220,7 @@ class TenantService: # Initialize an empty list to store the updated accounts updated_accounts = [] - for account, role in query: + for account, role in db.session.execute(stmt): account.role = role updated_accounts.append(account) @@ -1208,26 +1233,31 @@ class TenantService: raise ValueError("all roles must be TenantAccountRole") return ( - db.session.query(TenantAccountJoin) - .where(TenantAccountJoin.tenant_id == tenant.id, TenantAccountJoin.role.in_([role.value for role in roles])) - .first() + db.session.scalar( + select(TenantAccountJoin) + .where( + TenantAccountJoin.tenant_id == tenant.id, + TenantAccountJoin.role.in_([role.value for role in roles]), + ) + .limit(1) + ) is not None ) @staticmethod def get_user_role(account: Account, tenant: Tenant) -> TenantAccountRole | None: """Get the role of the current account for a given tenant""" - join = ( - db.session.query(TenantAccountJoin) + join = db.session.scalar( + select(TenantAccountJoin) .where(TenantAccountJoin.tenant_id == tenant.id, TenantAccountJoin.account_id == account.id) - .first() + .limit(1) ) return TenantAccountRole(join.role) if join else None @staticmethod def get_tenant_count() -> int: """Get tenant count""" - return cast(int, db.session.query(func.count(Tenant.id)).scalar()) + return cast(int, db.session.scalar(select(func.count(Tenant.id)))) @staticmethod def check_member_permission(tenant: Tenant, operator: Account, member: Account | None, action: str): @@ -1244,7 +1274,11 @@ class TenantService: if operator.id == member.id: raise CannotOperateSelfError("Cannot operate self.") - ta_operator = db.session.query(TenantAccountJoin).filter_by(tenant_id=tenant.id, account_id=operator.id).first() + ta_operator = db.session.scalar( + select(TenantAccountJoin) + .where(TenantAccountJoin.tenant_id == tenant.id, TenantAccountJoin.account_id == operator.id) + .limit(1) + ) if not ta_operator or ta_operator.role not in perms[action]: raise NoPermissionError(f"No permission to {action} member.") @@ -1262,7 +1296,11 @@ class TenantService: TenantService.check_member_permission(tenant, operator, account, "remove") - ta = db.session.query(TenantAccountJoin).filter_by(tenant_id=tenant.id, account_id=account.id).first() + ta = db.session.scalar( + select(TenantAccountJoin) + .where(TenantAccountJoin.tenant_id == tenant.id, TenantAccountJoin.account_id == account.id) + .limit(1) + ) if not ta: raise MemberNotInTenantError("Member not in tenant.") @@ -1277,7 +1315,12 @@ class TenantService: should_delete_account = False if account.status == AccountStatus.PENDING: # autoflush flushes ta deletion before this query, so 0 means no remaining joins - remaining_joins = db.session.query(TenantAccountJoin).filter_by(account_id=account_id).count() + remaining_joins = ( + db.session.scalar( + select(func.count(TenantAccountJoin.id)).where(TenantAccountJoin.account_id == account_id) + ) + or 0 + ) if remaining_joins == 0: db.session.delete(account) should_delete_account = True @@ -1312,8 +1355,10 @@ class TenantService: """Update member role""" TenantService.check_member_permission(tenant, operator, member, "update") - target_member_join = ( - db.session.query(TenantAccountJoin).filter_by(tenant_id=tenant.id, account_id=member.id).first() + target_member_join = db.session.scalar( + select(TenantAccountJoin) + .where(TenantAccountJoin.tenant_id == tenant.id, TenantAccountJoin.account_id == member.id) + .limit(1) ) if not target_member_join: @@ -1324,8 +1369,10 @@ class TenantService: if new_role == "owner": # Find the current owner and change their role to 'admin' - current_owner_join = ( - db.session.query(TenantAccountJoin).filter_by(tenant_id=tenant.id, role="owner").first() + current_owner_join = db.session.scalar( + select(TenantAccountJoin) + .where(TenantAccountJoin.tenant_id == tenant.id, TenantAccountJoin.role == "owner") + .limit(1) ) if current_owner_join: current_owner_join.role = TenantAccountRole.ADMIN @@ -1384,10 +1431,10 @@ class RegisterService: db.session.add(dify_setup) db.session.commit() except Exception as e: - db.session.query(DifySetup).delete() - db.session.query(TenantAccountJoin).delete() - db.session.query(Account).delete() - db.session.query(Tenant).delete() + db.session.execute(delete(DifySetup)) + db.session.execute(delete(TenantAccountJoin)) + db.session.execute(delete(Account)) + db.session.execute(delete(Tenant)) db.session.commit() logger.exception("Setup account failed, email: %s, name: %s", email, name) @@ -1469,7 +1516,7 @@ class RegisterService: check_workspace_member_invite_permission(tenant.id) - with Session(db.engine) as session: + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: account = AccountService.get_account_by_email_with_case_fallback(email, session=session) if not account: @@ -1488,7 +1535,11 @@ class RegisterService: TenantService.switch_tenant(account, tenant.id) else: TenantService.check_member_permission(tenant, inviter, account, "add") - ta = db.session.query(TenantAccountJoin).filter_by(tenant_id=tenant.id, account_id=account.id).first() + ta = db.session.scalar( + select(TenantAccountJoin) + .where(TenantAccountJoin.tenant_id == tenant.id, TenantAccountJoin.account_id == account.id) + .limit(1) + ) if not ta: TenantService.create_tenant_member(tenant, account, role) @@ -1540,26 +1591,23 @@ class RegisterService: @classmethod def get_invitation_if_token_valid( cls, workspace_id: str | None, email: str | None, token: str - ) -> dict[str, Any] | None: + ) -> InvitationDetailDict | None: invitation_data = cls.get_invitation_by_token(token, workspace_id, email) if not invitation_data: return None - tenant = ( - db.session.query(Tenant) - .where(Tenant.id == invitation_data["workspace_id"], Tenant.status == "normal") - .first() + tenant = db.session.scalar( + select(Tenant).where(Tenant.id == invitation_data["workspace_id"], Tenant.status == "normal").limit(1) ) if not tenant: return None - tenant_account = ( - db.session.query(Account, TenantAccountJoin.role) + tenant_account = db.session.execute( + select(Account, TenantAccountJoin.role) .join(TenantAccountJoin, Account.id == TenantAccountJoin.account_id) .where(Account.email == invitation_data["email"], TenantAccountJoin.tenant_id == tenant.id) - .first() - ) + ).first() if not tenant_account: return None @@ -1605,7 +1653,7 @@ class RegisterService: @classmethod def get_invitation_with_case_fallback( cls, workspace_id: str | None, email: str | None, token: str - ) -> dict[str, Any] | None: + ) -> InvitationDetailDict | None: invitation = cls.get_invitation_if_token_valid(workspace_id, email, token) if invitation or not email or email == email.lower(): return invitation diff --git a/api/services/advanced_prompt_template_service.py b/api/services/advanced_prompt_template_service.py index f2ffa3b170..a6e6b1bae7 100644 --- a/api/services/advanced_prompt_template_service.py +++ b/api/services/advanced_prompt_template_service.py @@ -32,22 +32,33 @@ class AdvancedPromptTemplateService: def get_common_prompt(cls, app_mode: str, model_mode: str, has_context: str): context_prompt = copy.deepcopy(CONTEXT) - if app_mode == AppMode.CHAT: - if model_mode == "completion": - return cls.get_completion_prompt( - copy.deepcopy(CHAT_APP_COMPLETION_PROMPT_CONFIG), has_context, context_prompt - ) - elif model_mode == "chat": - return cls.get_chat_prompt(copy.deepcopy(CHAT_APP_CHAT_PROMPT_CONFIG), has_context, context_prompt) - elif app_mode == AppMode.COMPLETION: - if model_mode == "completion": - return cls.get_completion_prompt( - copy.deepcopy(COMPLETION_APP_COMPLETION_PROMPT_CONFIG), has_context, context_prompt - ) - elif model_mode == "chat": - return cls.get_chat_prompt( - copy.deepcopy(COMPLETION_APP_CHAT_PROMPT_CONFIG), has_context, context_prompt - ) + match app_mode: + case AppMode.CHAT: + match model_mode: + case "completion": + return cls.get_completion_prompt( + copy.deepcopy(CHAT_APP_COMPLETION_PROMPT_CONFIG), has_context, context_prompt + ) + case "chat": + return cls.get_chat_prompt( + copy.deepcopy(CHAT_APP_CHAT_PROMPT_CONFIG), has_context, context_prompt + ) + case _: + pass + case AppMode.COMPLETION: + match model_mode: + case "completion": + return cls.get_completion_prompt( + copy.deepcopy(COMPLETION_APP_COMPLETION_PROMPT_CONFIG), has_context, context_prompt + ) + case "chat": + return cls.get_chat_prompt( + copy.deepcopy(COMPLETION_APP_CHAT_PROMPT_CONFIG), has_context, context_prompt + ) + case _: + pass + case _: + pass # default return empty dict return {} @@ -73,25 +84,38 @@ class AdvancedPromptTemplateService: def get_baichuan_prompt(cls, app_mode: str, model_mode: str, has_context: str): baichuan_context_prompt = copy.deepcopy(BAICHUAN_CONTEXT) - if app_mode == AppMode.CHAT: - if model_mode == "completion": - return cls.get_completion_prompt( - copy.deepcopy(BAICHUAN_CHAT_APP_COMPLETION_PROMPT_CONFIG), has_context, baichuan_context_prompt - ) - elif model_mode == "chat": - return cls.get_chat_prompt( - copy.deepcopy(BAICHUAN_CHAT_APP_CHAT_PROMPT_CONFIG), has_context, baichuan_context_prompt - ) - elif app_mode == AppMode.COMPLETION: - if model_mode == "completion": - return cls.get_completion_prompt( - copy.deepcopy(BAICHUAN_COMPLETION_APP_COMPLETION_PROMPT_CONFIG), - has_context, - baichuan_context_prompt, - ) - elif model_mode == "chat": - return cls.get_chat_prompt( - copy.deepcopy(BAICHUAN_COMPLETION_APP_CHAT_PROMPT_CONFIG), has_context, baichuan_context_prompt - ) + match app_mode: + case AppMode.CHAT: + match model_mode: + case "completion": + return cls.get_completion_prompt( + copy.deepcopy(BAICHUAN_CHAT_APP_COMPLETION_PROMPT_CONFIG), + has_context, + baichuan_context_prompt, + ) + case "chat": + return cls.get_chat_prompt( + copy.deepcopy(BAICHUAN_CHAT_APP_CHAT_PROMPT_CONFIG), has_context, baichuan_context_prompt + ) + case _: + pass + case AppMode.COMPLETION: + match model_mode: + case "completion": + return cls.get_completion_prompt( + copy.deepcopy(BAICHUAN_COMPLETION_APP_COMPLETION_PROMPT_CONFIG), + has_context, + baichuan_context_prompt, + ) + case "chat": + return cls.get_chat_prompt( + copy.deepcopy(BAICHUAN_COMPLETION_APP_CHAT_PROMPT_CONFIG), + has_context, + baichuan_context_prompt, + ) + case _: + pass + case _: + pass # default return empty dict return {} diff --git a/api/services/annotation_service.py b/api/services/annotation_service.py index 8ebc87a670..ae5facbec0 100644 --- a/api/services/annotation_service.py +++ b/api/services/annotation_service.py @@ -4,7 +4,9 @@ import uuid import pandas as pd logger = logging.getLogger(__name__) -from sqlalchemy import or_, select +from typing import TypedDict + +from sqlalchemy import delete, or_, select, update from werkzeug.datastructures import FileStorage from werkzeug.exceptions import NotFound @@ -23,15 +25,34 @@ from tasks.annotation.enable_annotation_reply_task import enable_annotation_repl from tasks.annotation.update_annotation_to_index_task import update_annotation_to_index_task +class AnnotationJobStatusDict(TypedDict): + job_id: str + job_status: str + + +class EmbeddingModelDict(TypedDict): + embedding_provider_name: str + embedding_model_name: str + + +class AnnotationSettingDict(TypedDict): + id: str + enabled: bool + score_threshold: float + embedding_model: EmbeddingModelDict | dict + + +class AnnotationSettingDisabledDict(TypedDict): + enabled: bool + + class AppAnnotationService: @classmethod def up_insert_app_annotation_from_message(cls, args: dict, app_id: str) -> MessageAnnotation: # get app info current_user, current_tenant_id = current_account_with_tenant() - app = ( - db.session.query(App) - .where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal") - .first() + app = db.session.scalar( + select(App).where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal").limit(1) ) if not app: @@ -43,7 +64,9 @@ class AppAnnotationService: if args.get("message_id"): message_id = str(args["message_id"]) - message = db.session.query(Message).where(Message.id == message_id, Message.app_id == app.id).first() + message = db.session.scalar( + select(Message).where(Message.id == message_id, Message.app_id == app.id).limit(1) + ) if not message: raise NotFound("Message Not Exists.") @@ -72,7 +95,9 @@ class AppAnnotationService: db.session.add(annotation) db.session.commit() - annotation_setting = db.session.query(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).first() + annotation_setting = db.session.scalar( + select(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).limit(1) + ) assert current_tenant_id is not None if annotation_setting: add_annotation_to_index_task.delay( @@ -85,7 +110,7 @@ class AppAnnotationService: return annotation @classmethod - def enable_app_annotation(cls, args: dict, app_id: str): + def enable_app_annotation(cls, args: dict, app_id: str) -> AnnotationJobStatusDict: enable_app_annotation_key = f"enable_app_annotation_{str(app_id)}" cache_result = redis_client.get(enable_app_annotation_key) if cache_result is not None: @@ -109,7 +134,7 @@ class AppAnnotationService: return {"job_id": job_id, "job_status": "waiting"} @classmethod - def disable_app_annotation(cls, app_id: str): + def disable_app_annotation(cls, app_id: str) -> AnnotationJobStatusDict: _, current_tenant_id = current_account_with_tenant() disable_app_annotation_key = f"disable_app_annotation_{str(app_id)}" cache_result = redis_client.get(disable_app_annotation_key) @@ -128,10 +153,8 @@ class AppAnnotationService: def get_annotation_list_by_app_id(cls, app_id: str, page: int, limit: int, keyword: str): # get app info _, current_tenant_id = current_account_with_tenant() - app = ( - db.session.query(App) - .where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal") - .first() + app = db.session.scalar( + select(App).where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal").limit(1) ) if not app: @@ -170,20 +193,17 @@ class AppAnnotationService: """ # get app info _, current_tenant_id = current_account_with_tenant() - app = ( - db.session.query(App) - .where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal") - .first() + app = db.session.scalar( + select(App).where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal").limit(1) ) if not app: raise NotFound("App not found") - annotations = ( - db.session.query(MessageAnnotation) + annotations = db.session.scalars( + select(MessageAnnotation) .where(MessageAnnotation.app_id == app_id) .order_by(MessageAnnotation.created_at.desc()) - .all() - ) + ).all() # Sanitize CSV-injectable fields to prevent formula injection for annotation in annotations: @@ -200,10 +220,8 @@ class AppAnnotationService: def insert_app_annotation_directly(cls, args: dict, app_id: str) -> MessageAnnotation: # get app info current_user, current_tenant_id = current_account_with_tenant() - app = ( - db.session.query(App) - .where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal") - .first() + app = db.session.scalar( + select(App).where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal").limit(1) ) if not app: @@ -219,7 +237,9 @@ class AppAnnotationService: db.session.add(annotation) db.session.commit() # if annotation reply is enabled , add annotation to index - annotation_setting = db.session.query(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).first() + annotation_setting = db.session.scalar( + select(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).limit(1) + ) if annotation_setting: add_annotation_to_index_task.delay( annotation.id, @@ -234,16 +254,14 @@ class AppAnnotationService: def update_app_annotation_directly(cls, args: dict, app_id: str, annotation_id: str): # get app info _, current_tenant_id = current_account_with_tenant() - app = ( - db.session.query(App) - .where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal") - .first() + app = db.session.scalar( + select(App).where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal").limit(1) ) if not app: raise NotFound("App not found") - annotation = db.session.query(MessageAnnotation).where(MessageAnnotation.id == annotation_id).first() + annotation = db.session.get(MessageAnnotation, annotation_id) if not annotation: raise NotFound("Annotation not found") @@ -257,8 +275,8 @@ class AppAnnotationService: db.session.commit() # if annotation reply is enabled , add annotation to index - app_annotation_setting = ( - db.session.query(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).first() + app_annotation_setting = db.session.scalar( + select(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).limit(1) ) if app_annotation_setting: @@ -276,16 +294,14 @@ class AppAnnotationService: def delete_app_annotation(cls, app_id: str, annotation_id: str): # get app info _, current_tenant_id = current_account_with_tenant() - app = ( - db.session.query(App) - .where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal") - .first() + app = db.session.scalar( + select(App).where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal").limit(1) ) if not app: raise NotFound("App not found") - annotation = db.session.query(MessageAnnotation).where(MessageAnnotation.id == annotation_id).first() + annotation = db.session.get(MessageAnnotation, annotation_id) if not annotation: raise NotFound("Annotation not found") @@ -301,8 +317,8 @@ class AppAnnotationService: db.session.commit() # if annotation reply is enabled , delete annotation index - app_annotation_setting = ( - db.session.query(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).first() + app_annotation_setting = db.session.scalar( + select(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).limit(1) ) if app_annotation_setting: @@ -314,22 +330,19 @@ class AppAnnotationService: def delete_app_annotations_in_batch(cls, app_id: str, annotation_ids: list[str]): # get app info _, current_tenant_id = current_account_with_tenant() - app = ( - db.session.query(App) - .where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal") - .first() + app = db.session.scalar( + select(App).where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal").limit(1) ) if not app: raise NotFound("App not found") # Fetch annotations and their settings in a single query - annotations_to_delete = ( - db.session.query(MessageAnnotation, AppAnnotationSetting) + annotations_to_delete = db.session.execute( + select(MessageAnnotation, AppAnnotationSetting) .outerjoin(AppAnnotationSetting, MessageAnnotation.app_id == AppAnnotationSetting.app_id) .where(MessageAnnotation.id.in_(annotation_ids)) - .all() - ) + ).all() if not annotations_to_delete: return {"deleted_count": 0} @@ -338,9 +351,9 @@ class AppAnnotationService: annotation_ids_to_delete = [annotation.id for annotation, _ in annotations_to_delete] # Step 2: Bulk delete hit histories in a single query - db.session.query(AppAnnotationHitHistory).where( - AppAnnotationHitHistory.annotation_id.in_(annotation_ids_to_delete) - ).delete(synchronize_session=False) + db.session.execute( + delete(AppAnnotationHitHistory).where(AppAnnotationHitHistory.annotation_id.in_(annotation_ids_to_delete)) + ) # Step 3: Trigger async tasks for search index deletion for annotation, annotation_setting in annotations_to_delete: @@ -350,11 +363,10 @@ class AppAnnotationService: ) # Step 4: Bulk delete annotations in a single query - deleted_count = ( - db.session.query(MessageAnnotation) - .where(MessageAnnotation.id.in_(annotation_ids_to_delete)) - .delete(synchronize_session=False) + delete_result = db.session.execute( + delete(MessageAnnotation).where(MessageAnnotation.id.in_(annotation_ids_to_delete)) ) + deleted_count = getattr(delete_result, "rowcount", 0) db.session.commit() return {"deleted_count": deleted_count} @@ -375,10 +387,8 @@ class AppAnnotationService: # get app info current_user, current_tenant_id = current_account_with_tenant() - app = ( - db.session.query(App) - .where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal") - .first() + app = db.session.scalar( + select(App).where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal").limit(1) ) if not app: @@ -499,16 +509,14 @@ class AppAnnotationService: def get_annotation_hit_histories(cls, app_id: str, annotation_id: str, page, limit): _, current_tenant_id = current_account_with_tenant() # get app info - app = ( - db.session.query(App) - .where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal") - .first() + app = db.session.scalar( + select(App).where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal").limit(1) ) if not app: raise NotFound("App not found") - annotation = db.session.query(MessageAnnotation).where(MessageAnnotation.id == annotation_id).first() + annotation = db.session.get(MessageAnnotation, annotation_id) if not annotation: raise NotFound("Annotation not found") @@ -528,7 +536,7 @@ class AppAnnotationService: @classmethod def get_annotation_by_id(cls, annotation_id: str) -> MessageAnnotation | None: - annotation = db.session.query(MessageAnnotation).where(MessageAnnotation.id == annotation_id).first() + annotation = db.session.get(MessageAnnotation, annotation_id) if not annotation: return None @@ -548,8 +556,10 @@ class AppAnnotationService: score: float, ): # add hit count to annotation - db.session.query(MessageAnnotation).where(MessageAnnotation.id == annotation_id).update( - {MessageAnnotation.hit_count: MessageAnnotation.hit_count + 1}, synchronize_session=False + db.session.execute( + update(MessageAnnotation) + .where(MessageAnnotation.id == annotation_id) + .values(hit_count=MessageAnnotation.hit_count + 1) ) annotation_hit_history = AppAnnotationHitHistory( @@ -567,19 +577,19 @@ class AppAnnotationService: db.session.commit() @classmethod - def get_app_annotation_setting_by_app_id(cls, app_id: str): + def get_app_annotation_setting_by_app_id(cls, app_id: str) -> AnnotationSettingDict | AnnotationSettingDisabledDict: _, current_tenant_id = current_account_with_tenant() # get app info - app = ( - db.session.query(App) - .where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal") - .first() + app = db.session.scalar( + select(App).where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal").limit(1) ) if not app: raise NotFound("App not found") - annotation_setting = db.session.query(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).first() + annotation_setting = db.session.scalar( + select(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).limit(1) + ) if annotation_setting: collection_binding_detail = annotation_setting.collection_binding_detail if collection_binding_detail: @@ -602,25 +612,25 @@ class AppAnnotationService: return {"enabled": False} @classmethod - def update_app_annotation_setting(cls, app_id: str, annotation_setting_id: str, args: dict): + def update_app_annotation_setting( + cls, app_id: str, annotation_setting_id: str, args: dict + ) -> AnnotationSettingDict: current_user, current_tenant_id = current_account_with_tenant() # get app info - app = ( - db.session.query(App) - .where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal") - .first() + app = db.session.scalar( + select(App).where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal").limit(1) ) if not app: raise NotFound("App not found") - annotation_setting = ( - db.session.query(AppAnnotationSetting) + annotation_setting = db.session.scalar( + select(AppAnnotationSetting) .where( AppAnnotationSetting.app_id == app_id, AppAnnotationSetting.id == annotation_setting_id, ) - .first() + .limit(1) ) if not annotation_setting: raise NotFound("App annotation not found") @@ -653,26 +663,26 @@ class AppAnnotationService: @classmethod def clear_all_annotations(cls, app_id: str): _, current_tenant_id = current_account_with_tenant() - app = ( - db.session.query(App) - .where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal") - .first() + app = db.session.scalar( + select(App).where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal").limit(1) ) if not app: raise NotFound("App not found") # if annotation reply is enabled, delete annotation index - app_annotation_setting = ( - db.session.query(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).first() + app_annotation_setting = db.session.scalar( + select(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).limit(1) ) - annotations_query = db.session.query(MessageAnnotation).where(MessageAnnotation.app_id == app_id) - for annotation in annotations_query.yield_per(100): - annotation_hit_histories_query = db.session.query(AppAnnotationHitHistory).where( - AppAnnotationHitHistory.annotation_id == annotation.id - ) - for annotation_hit_history in annotation_hit_histories_query.yield_per(100): + annotations_iter = db.session.scalars( + select(MessageAnnotation).where(MessageAnnotation.app_id == app_id) + ).yield_per(100) + for annotation in annotations_iter: + hit_histories_iter = db.session.scalars( + select(AppAnnotationHitHistory).where(AppAnnotationHitHistory.annotation_id == annotation.id) + ).yield_per(100) + for annotation_hit_history in hit_histories_iter: db.session.delete(annotation_hit_history) # if annotation reply is enabled, delete annotation index diff --git a/api/services/app_generate_service.py b/api/services/app_generate_service.py index 5bff841c10..a6639dc780 100644 --- a/api/services/app_generate_service.py +++ b/api/services/app_generate_service.py @@ -118,139 +118,143 @@ class AppGenerateService: try: request_id = rate_limit.enter(request_id) quota_charge.commit() - if app_model.mode == AppMode.COMPLETION: - return rate_limit.generate( - CompletionAppGenerator.convert_to_event_stream( - CompletionAppGenerator().generate( - app_model=app_model, user=user, args=args, invoke_from=invoke_from, streaming=streaming - ), - ), - request_id=request_id, - ) - elif app_model.mode == AppMode.AGENT_CHAT or app_model.is_agent: - return rate_limit.generate( - AgentChatAppGenerator.convert_to_event_stream( - AgentChatAppGenerator().generate( - app_model=app_model, user=user, args=args, invoke_from=invoke_from, streaming=streaming - ), - ), - request_id, - ) - elif app_model.mode == AppMode.CHAT: - return rate_limit.generate( - ChatAppGenerator.convert_to_event_stream( - ChatAppGenerator().generate( - app_model=app_model, user=user, args=args, invoke_from=invoke_from, streaming=streaming - ), - ), - request_id=request_id, - ) - elif app_model.mode == AppMode.ADVANCED_CHAT: - workflow_id = args.get("workflow_id") - workflow = cls._get_workflow(app_model, invoke_from, workflow_id) - - if streaming: - # Streaming mode: subscribe to SSE and enqueue the execution on first subscriber - with rate_limit_context(rate_limit, request_id): - payload = AppExecutionParams.new( - app_model=app_model, - workflow=workflow, - user=user, - args=args, - invoke_from=invoke_from, - streaming=True, - call_depth=0, - ) - payload_json = payload.model_dump_json() - - def on_subscribe(): - workflow_based_app_execution_task.delay(payload_json) - - on_subscribe = cls._build_streaming_task_on_subscribe(on_subscribe) - generator = AdvancedChatAppGenerator() + effective_mode = ( + AppMode.AGENT_CHAT if app_model.is_agent and app_model.mode != AppMode.AGENT_CHAT else app_model.mode + ) + match effective_mode: + case AppMode.COMPLETION: return rate_limit.generate( - generator.convert_to_event_stream( - generator.retrieve_events( - AppMode.ADVANCED_CHAT, - payload.workflow_run_id, - on_subscribe=on_subscribe, + CompletionAppGenerator.convert_to_event_stream( + CompletionAppGenerator().generate( + app_model=app_model, user=user, args=args, invoke_from=invoke_from, streaming=streaming ), ), request_id=request_id, ) - else: - # Blocking mode: run synchronously and return JSON instead of SSE - # Keep behaviour consistent with WORKFLOW blocking branch. - advanced_generator = AdvancedChatAppGenerator() + case AppMode.AGENT_CHAT: return rate_limit.generate( - advanced_generator.convert_to_event_stream( - advanced_generator.generate( + AgentChatAppGenerator.convert_to_event_stream( + AgentChatAppGenerator().generate( + app_model=app_model, user=user, args=args, invoke_from=invoke_from, streaming=streaming + ), + ), + request_id, + ) + case AppMode.CHAT: + return rate_limit.generate( + ChatAppGenerator.convert_to_event_stream( + ChatAppGenerator().generate( + app_model=app_model, user=user, args=args, invoke_from=invoke_from, streaming=streaming + ), + ), + request_id=request_id, + ) + case AppMode.ADVANCED_CHAT: + workflow_id = args.get("workflow_id") + workflow = cls._get_workflow(app_model, invoke_from, workflow_id) + + if streaming: + # Streaming mode: subscribe to SSE and enqueue the execution on first subscriber + with rate_limit_context(rate_limit, request_id): + payload = AppExecutionParams.new( app_model=app_model, workflow=workflow, user=user, args=args, invoke_from=invoke_from, - workflow_run_id=str(uuid.uuid4()), - streaming=False, + streaming=True, + call_depth=0, ) - ), - request_id=request_id, - ) - elif app_model.mode == AppMode.WORKFLOW: - workflow_id = args.get("workflow_id") - workflow = cls._get_workflow(app_model, invoke_from, workflow_id) - if streaming: - with rate_limit_context(rate_limit, request_id): - payload = AppExecutionParams.new( - app_model=app_model, - workflow=workflow, - user=user, - args=args, - invoke_from=invoke_from, - streaming=True, - call_depth=0, - root_node_id=root_node_id, - workflow_run_id=str(uuid.uuid4()), + payload_json = payload.model_dump_json() + + def on_subscribe(): + workflow_based_app_execution_task.delay(payload_json) + + on_subscribe = cls._build_streaming_task_on_subscribe(on_subscribe) + generator = AdvancedChatAppGenerator() + return rate_limit.generate( + generator.convert_to_event_stream( + generator.retrieve_events( + AppMode.ADVANCED_CHAT, + payload.workflow_run_id, + on_subscribe=on_subscribe, + ), + ), + request_id=request_id, ) - payload_json = payload.model_dump_json() + else: + # Blocking mode: run synchronously and return JSON instead of SSE + # Keep behaviour consistent with WORKFLOW blocking branch. + advanced_generator = AdvancedChatAppGenerator() + return rate_limit.generate( + advanced_generator.convert_to_event_stream( + advanced_generator.generate( + app_model=app_model, + workflow=workflow, + user=user, + args=args, + invoke_from=invoke_from, + workflow_run_id=str(uuid.uuid4()), + streaming=False, + ) + ), + request_id=request_id, + ) + case AppMode.WORKFLOW: + workflow_id = args.get("workflow_id") + workflow = cls._get_workflow(app_model, invoke_from, workflow_id) + if streaming: + with rate_limit_context(rate_limit, request_id): + payload = AppExecutionParams.new( + app_model=app_model, + workflow=workflow, + user=user, + args=args, + invoke_from=invoke_from, + streaming=True, + call_depth=0, + root_node_id=root_node_id, + workflow_run_id=str(uuid.uuid4()), + ) + payload_json = payload.model_dump_json() - def on_subscribe(): - workflow_based_app_execution_task.delay(payload_json) + def on_subscribe(): + workflow_based_app_execution_task.delay(payload_json) - on_subscribe = cls._build_streaming_task_on_subscribe(on_subscribe) + on_subscribe = cls._build_streaming_task_on_subscribe(on_subscribe) + return rate_limit.generate( + WorkflowAppGenerator.convert_to_event_stream( + MessageBasedAppGenerator.retrieve_events( + AppMode.WORKFLOW, + payload.workflow_run_id, + on_subscribe=on_subscribe, + ), + ), + request_id, + ) + + pause_config = PauseStateLayerConfig( + session_factory=session_factory.get_session_maker(), + state_owner_user_id=workflow.created_by, + ) return rate_limit.generate( WorkflowAppGenerator.convert_to_event_stream( - MessageBasedAppGenerator.retrieve_events( - AppMode.WORKFLOW, - payload.workflow_run_id, - on_subscribe=on_subscribe, + WorkflowAppGenerator().generate( + app_model=app_model, + workflow=workflow, + user=user, + args=args, + invoke_from=invoke_from, + streaming=False, + root_node_id=root_node_id, + call_depth=0, + pause_state_config=pause_config, ), ), request_id, ) - - pause_config = PauseStateLayerConfig( - session_factory=session_factory.get_session_maker(), - state_owner_user_id=workflow.created_by, - ) - return rate_limit.generate( - WorkflowAppGenerator.convert_to_event_stream( - WorkflowAppGenerator().generate( - app_model=app_model, - workflow=workflow, - user=user, - args=args, - invoke_from=invoke_from, - streaming=False, - root_node_id=root_node_id, - call_depth=0, - pause_state_config=pause_config, - ), - ), - request_id, - ) - else: - raise ValueError(f"Invalid app mode {app_model.mode}") + case _: + raise ValueError(f"Invalid app mode {app_model.mode}") except Exception: quota_charge.refund() rate_limit.exit(request_id) @@ -282,43 +286,73 @@ class AppGenerateService: @classmethod def generate_single_iteration(cls, app_model: App, user: Account, node_id: str, args: Any, streaming: bool = True): - if app_model.mode == AppMode.ADVANCED_CHAT: - workflow = cls._get_workflow(app_model, InvokeFrom.DEBUGGER) - return AdvancedChatAppGenerator.convert_to_event_stream( - AdvancedChatAppGenerator().single_iteration_generate( - app_model=app_model, workflow=workflow, node_id=node_id, user=user, args=args, streaming=streaming + match app_model.mode: + case AppMode.COMPLETION | AppMode.CHAT | AppMode.AGENT_CHAT: + raise ValueError(f"Invalid app mode {app_model.mode}") + case AppMode.ADVANCED_CHAT: + workflow = cls._get_workflow(app_model, InvokeFrom.DEBUGGER) + return AdvancedChatAppGenerator.convert_to_event_stream( + AdvancedChatAppGenerator().single_iteration_generate( + app_model=app_model, + workflow=workflow, + node_id=node_id, + user=user, + args=args, + streaming=streaming, + ) ) - ) - elif app_model.mode == AppMode.WORKFLOW: - workflow = cls._get_workflow(app_model, InvokeFrom.DEBUGGER) - return AdvancedChatAppGenerator.convert_to_event_stream( - WorkflowAppGenerator().single_iteration_generate( - app_model=app_model, workflow=workflow, node_id=node_id, user=user, args=args, streaming=streaming + case AppMode.WORKFLOW: + workflow = cls._get_workflow(app_model, InvokeFrom.DEBUGGER) + return AdvancedChatAppGenerator.convert_to_event_stream( + WorkflowAppGenerator().single_iteration_generate( + app_model=app_model, + workflow=workflow, + node_id=node_id, + user=user, + args=args, + streaming=streaming, + ) ) - ) - else: - raise ValueError(f"Invalid app mode {app_model.mode}") + case AppMode.CHANNEL | AppMode.RAG_PIPELINE: + raise ValueError(f"Invalid app mode {app_model.mode}") + case _: + raise ValueError(f"Invalid app mode {app_model.mode}") @classmethod def generate_single_loop( cls, app_model: App, user: Account, node_id: str, args: LoopNodeRunPayload, streaming: bool = True ): - if app_model.mode == AppMode.ADVANCED_CHAT: - workflow = cls._get_workflow(app_model, InvokeFrom.DEBUGGER) - return AdvancedChatAppGenerator.convert_to_event_stream( - AdvancedChatAppGenerator().single_loop_generate( - app_model=app_model, workflow=workflow, node_id=node_id, user=user, args=args, streaming=streaming + match app_model.mode: + case AppMode.COMPLETION | AppMode.CHAT | AppMode.AGENT_CHAT: + raise ValueError(f"Invalid app mode {app_model.mode}") + case AppMode.ADVANCED_CHAT: + workflow = cls._get_workflow(app_model, InvokeFrom.DEBUGGER) + return AdvancedChatAppGenerator.convert_to_event_stream( + AdvancedChatAppGenerator().single_loop_generate( + app_model=app_model, + workflow=workflow, + node_id=node_id, + user=user, + args=args, + streaming=streaming, + ) ) - ) - elif app_model.mode == AppMode.WORKFLOW: - workflow = cls._get_workflow(app_model, InvokeFrom.DEBUGGER) - return AdvancedChatAppGenerator.convert_to_event_stream( - WorkflowAppGenerator().single_loop_generate( - app_model=app_model, workflow=workflow, node_id=node_id, user=user, args=args, streaming=streaming + case AppMode.WORKFLOW: + workflow = cls._get_workflow(app_model, InvokeFrom.DEBUGGER) + return AdvancedChatAppGenerator.convert_to_event_stream( + WorkflowAppGenerator().single_loop_generate( + app_model=app_model, + workflow=workflow, + node_id=node_id, + user=user, + args=args, + streaming=streaming, + ) ) - ) - else: - raise ValueError(f"Invalid app mode {app_model.mode}") + case AppMode.CHANNEL | AppMode.RAG_PIPELINE: + raise ValueError(f"Invalid app mode {app_model.mode}") + case _: + raise ValueError(f"Invalid app mode {app_model.mode}") @classmethod def generate_more_like_this( diff --git a/api/services/app_model_config_service.py b/api/services/app_model_config_service.py index 3bc30cb323..2013c869af 100644 --- a/api/services/app_model_config_service.py +++ b/api/services/app_model_config_service.py @@ -7,11 +7,12 @@ from models.model import AppMode, AppModelConfigDict class AppModelConfigService: @classmethod def validate_configuration(cls, tenant_id: str, config: dict, app_mode: AppMode) -> AppModelConfigDict: - if app_mode == AppMode.CHAT: - return ChatAppConfigManager.config_validate(tenant_id, config) - elif app_mode == AppMode.AGENT_CHAT: - return AgentChatAppConfigManager.config_validate(tenant_id, config) - elif app_mode == AppMode.COMPLETION: - return CompletionAppConfigManager.config_validate(tenant_id, config) - else: - raise ValueError(f"Invalid app mode: {app_mode}") + match app_mode: + case AppMode.CHAT: + return ChatAppConfigManager.config_validate(tenant_id, config) + case AppMode.AGENT_CHAT: + return AgentChatAppConfigManager.config_validate(tenant_id, config) + case AppMode.COMPLETION: + return CompletionAppConfigManager.config_validate(tenant_id, config) + case AppMode.WORKFLOW | AppMode.ADVANCED_CHAT | AppMode.CHANNEL | AppMode.RAG_PIPELINE: + raise ValueError(f"Invalid app mode: {app_mode}") diff --git a/api/services/async_workflow_service.py b/api/services/async_workflow_service.py index 327756753c..b4471f51d8 100644 --- a/api/services/async_workflow_service.py +++ b/api/services/async_workflow_service.py @@ -11,7 +11,7 @@ from typing import Any, Union from celery.result import AsyncResult from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, sessionmaker from enums.quota_type import QuotaType from extensions.ext_database import db @@ -244,7 +244,7 @@ class AsyncWorkflowService: Returns: Trigger log as dictionary or None if not found """ - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: trigger_log_repo = SQLAlchemyWorkflowTriggerLogRepository(session) trigger_log = trigger_log_repo.get_by_id(workflow_trigger_log_id, tenant_id) @@ -270,7 +270,7 @@ class AsyncWorkflowService: Returns: List of trigger logs as dictionaries """ - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: trigger_log_repo = SQLAlchemyWorkflowTriggerLogRepository(session) logs = trigger_log_repo.get_recent_logs( tenant_id=tenant_id, app_id=app_id, hours=hours, limit=limit, offset=offset @@ -293,7 +293,7 @@ class AsyncWorkflowService: Returns: List of failed trigger logs as dictionaries """ - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: trigger_log_repo = SQLAlchemyWorkflowTriggerLogRepository(session) logs = trigger_log_repo.get_failed_for_retry( tenant_id=tenant_id, max_retry_count=max_retry_count, limit=limit diff --git a/api/services/attachment_service.py b/api/services/attachment_service.py index 2bd5627d5e..54e664e944 100644 --- a/api/services/attachment_service.py +++ b/api/services/attachment_service.py @@ -1,6 +1,6 @@ import base64 -from sqlalchemy import Engine +from sqlalchemy import Engine, select from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import NotFound @@ -22,8 +22,8 @@ class AttachmentService: raise AssertionError("must be a sessionmaker or an Engine.") def get_file_base64(self, file_id: str) -> str: - upload_file = ( - self._session_maker(expire_on_commit=False).query(UploadFile).where(UploadFile.id == file_id).first() + upload_file = self._session_maker(expire_on_commit=False).scalar( + select(UploadFile).where(UploadFile.id == file_id).limit(1) ) if not upload_file: raise NotFound("File not found") diff --git a/api/services/billing_service.py b/api/services/billing_service.py index a183f09370..3dc7fff88e 100644 --- a/api/services/billing_service.py +++ b/api/services/billing_service.py @@ -107,6 +107,124 @@ class BillingInfo(TypedDict): _billing_info_adapter = TypeAdapter(BillingInfo) +class _TenantFeatureQuota(TypedDict): + usage: int + limit: int + reset_date: NotRequired[int] + + +class TenantFeatureQuotaInfo(TypedDict): + """Response of /quota/info. + + NOTE (hj24): + - Same convention as BillingInfo: billing may return int fields as str, + always keep non-strict mode to auto-coerce. + """ + + trigger_event: _TenantFeatureQuota + api_rate_limit: _TenantFeatureQuota + + +_tenant_feature_quota_info_adapter = TypeAdapter(TenantFeatureQuotaInfo) + + +class _BillingQuota(TypedDict): + size: int + limit: int + + +class _VectorSpaceQuota(TypedDict): + size: float + limit: int + + +class _KnowledgeRateLimit(TypedDict): + # NOTE (hj24): + # 1. Return for sandbox users but is null for other plans, it's defined but never used. + # 2. Keep it for compatibility for now, can be deprecated in future versions. + size: NotRequired[int] + # NOTE END + limit: int + + +class _BillingSubscription(TypedDict): + plan: str + interval: str + education: bool + + +class BillingInfo(TypedDict): + """Response of /subscription/info. + + NOTE (hj24): + - Fields not listed here (e.g. trigger_event, api_rate_limit) are stripped by TypeAdapter.validate_python() + - To ensure the precision, billing may convert fields like int as str, be careful when use TypeAdapter: + 1. validate_python in non-strict mode will coerce it to the expected type + 2. In strict mode, it will raise ValidationError + 3. To preserve compatibility, always keep non-strict mode here and avoid strict mode + """ + + enabled: bool + subscription: _BillingSubscription + members: _BillingQuota + apps: _BillingQuota + vector_space: _VectorSpaceQuota + knowledge_rate_limit: _KnowledgeRateLimit + documents_upload_quota: _BillingQuota + annotation_quota_limit: _BillingQuota + docs_processing: str + can_replace_logo: bool + model_load_balancing_enabled: bool + knowledge_pipeline_publish_enabled: bool + next_credit_reset_date: NotRequired[int] + + +_billing_info_adapter = TypeAdapter(BillingInfo) + + +class KnowledgeRateLimitDict(TypedDict): + limit: int + subscription_plan: str + + +class TenantFeaturePlanUsageDict(TypedDict): + result: str + history_id: str + + +class LangContentDict(TypedDict): + lang: str + title: str + subtitle: str + body: str + title_pic_url: str + + +class NotificationDict(TypedDict): + notification_id: str + contents: dict[str, LangContentDict] + frequency: Literal["once", "every_page_load"] + + +class AccountNotificationDict(TypedDict, total=False): + should_show: bool + notification: NotificationDict + shouldShow: bool + notifications: list[dict] + + +class UpsertNotificationDict(TypedDict): + notification_id: str + + +class BatchAddNotificationAccountsDict(TypedDict): + count: int + + +class DismissNotificationDict(TypedDict): + success: bool + + class BillingService: base_url = os.environ.get("BILLING_API_URL", "BILLING_API_URL") secret_key = os.environ.get("BILLING_API_SECRET_KEY", "BILLING_API_SECRET_KEY") @@ -133,9 +251,11 @@ class BillingService: return usage_info @classmethod - def get_quota_info(cls, tenant_id: str): + def get_quota_info(cls, tenant_id: str) -> TenantFeatureQuotaInfo: params = {"tenant_id": tenant_id} - return cls._send_request("GET", "/quota/info", params=params) + return _tenant_feature_quota_info_adapter.validate_python( + cls._send_request("GET", "/quota/info", params=params) + ) @classmethod def quota_reserve( @@ -183,7 +303,7 @@ class BillingService: ) @classmethod - def get_knowledge_rate_limit(cls, tenant_id: str): + def get_knowledge_rate_limit(cls, tenant_id: str) -> KnowledgeRateLimitDict: params = {"tenant_id": tenant_id} knowledge_rate_limit = cls._send_request("GET", "/subscription/knowledge-rate-limit", params=params) @@ -214,7 +334,9 @@ class BillingService: return cls._send_request("GET", "/invoices", params=params) @classmethod - def update_tenant_feature_plan_usage(cls, tenant_id: str, feature_key: str, delta: int) -> dict: + def update_tenant_feature_plan_usage( + cls, tenant_id: str, feature_key: str, delta: int + ) -> TenantFeaturePlanUsageDict: """ Update tenant feature plan usage. @@ -234,7 +356,7 @@ class BillingService: ) @classmethod - def refund_tenant_feature_plan_usage(cls, history_id: str) -> dict: + def refund_tenant_feature_plan_usage(cls, history_id: str) -> TenantFeaturePlanUsageDict: """ Refund a previous usage charge. @@ -530,7 +652,7 @@ class BillingService: return tenant_whitelist @classmethod - def get_account_notification(cls, account_id: str) -> dict: + def get_account_notification(cls, account_id: str) -> AccountNotificationDict: """Return the active in-product notification for account_id, if any. Calling this endpoint also marks the notification as seen; subsequent @@ -554,13 +676,13 @@ class BillingService: @classmethod def upsert_notification( cls, - contents: list[dict], + contents: list[LangContentDict], frequency: str = "once", status: str = "active", notification_id: str | None = None, start_time: str | None = None, end_time: str | None = None, - ) -> dict: + ) -> UpsertNotificationDict: """Create or update a notification. contents: list of {"lang": str, "title": str, "subtitle": str, "body": str, "title_pic_url": str} @@ -581,7 +703,9 @@ class BillingService: return cls._send_request("POST", "/notifications", json=payload) @classmethod - def batch_add_notification_accounts(cls, notification_id: str, account_ids: list[str]) -> dict: + def batch_add_notification_accounts( + cls, notification_id: str, account_ids: list[str] + ) -> BatchAddNotificationAccountsDict: """Register target account IDs for a notification (max 1000 per call). Returns {"count": int}. @@ -593,7 +717,7 @@ class BillingService: ) @classmethod - def dismiss_notification(cls, notification_id: str, account_id: str) -> dict: + def dismiss_notification(cls, notification_id: str, account_id: str) -> DismissNotificationDict: """Mark a notification as dismissed for an account. Returns {"success": bool}. diff --git a/api/services/clear_free_plan_tenant_expired_logs.py b/api/services/clear_free_plan_tenant_expired_logs.py index 1c128524ad..b4a7fa051f 100644 --- a/api/services/clear_free_plan_tenant_expired_logs.py +++ b/api/services/clear_free_plan_tenant_expired_logs.py @@ -346,7 +346,7 @@ class ClearFreePlanTenantExpiredLogs: started_at = datetime.datetime(2023, 4, 3, 8, 59, 24) current_time = started_at - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: total_tenant_count = session.query(Tenant.id).count() click.echo(click.style(f"Total tenant count: {total_tenant_count}", fg="white")) @@ -398,7 +398,7 @@ class ClearFreePlanTenantExpiredLogs: # Initial interval of 1 day, will be dynamically adjusted based on tenant count interval = datetime.timedelta(days=1) # Process tenants in this batch - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: # Calculate tenant count in next batch with current interval # Try different intervals until we find one with a reasonable tenant count test_intervals = [ diff --git a/api/services/credit_pool_service.py b/api/services/credit_pool_service.py index 7826695366..16788300d3 100644 --- a/api/services/credit_pool_service.py +++ b/api/services/credit_pool_service.py @@ -1,7 +1,7 @@ import logging from sqlalchemy import select, update -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from configs import dify_config from core.errors.error import QuotaExceededError @@ -71,7 +71,7 @@ class CreditPoolService: actual_credits = min(credits_required, pool.remaining_credits) try: - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: stmt = ( update(TenantCreditPool) .where( @@ -81,7 +81,6 @@ class CreditPoolService: .values(quota_used=TenantCreditPool.quota_used + actual_credits) ) session.execute(stmt) - session.commit() except Exception: logger.exception("Failed to deduct credits for tenant %s", tenant_id) raise QuotaExceededError("Failed to deduct credits") diff --git a/api/services/dataset_service.py b/api/services/dataset_service.py index 53bc51d457..3e952059ac 100644 --- a/api/services/dataset_service.py +++ b/api/services/dataset_service.py @@ -7,15 +7,15 @@ import time import uuid from collections import Counter from collections.abc import Sequence -from typing import Any, Literal, cast +from typing import Any, Literal, TypedDict, cast import sqlalchemy as sa from graphon.file import helpers as file_helpers from graphon.model_runtime.entities.model_entities import ModelFeature, ModelType from graphon.model_runtime.model_providers.__base.text_embedding_model import TextEmbeddingModel from redis.exceptions import LockNotOwnedError -from sqlalchemy import exists, func, select -from sqlalchemy.orm import Session +from sqlalchemy import delete, exists, func, select, update +from sqlalchemy.orm import Session, sessionmaker from werkzeug.exceptions import Forbidden, NotFound from configs import dify_config @@ -107,6 +107,16 @@ from tasks.sync_website_document_indexing_task import sync_website_document_inde logger = logging.getLogger(__name__) +class ProcessRulesDict(TypedDict): + mode: str + rules: dict[str, Any] + + +class AutoDisableLogsDict(TypedDict): + document_ids: list[str] + count: int + + class DatasetService: @staticmethod def get_datasets(page, per_page, tenant_id=None, user=None, search=None, tag_ids=None, include_all=False): @@ -114,9 +124,11 @@ class DatasetService: if user: # get permitted dataset ids - dataset_permission = ( - db.session.query(DatasetPermission).filter_by(account_id=user.id, tenant_id=tenant_id).all() - ) + dataset_permission = db.session.scalars( + select(DatasetPermission).where( + DatasetPermission.account_id == user.id, DatasetPermission.tenant_id == tenant_id + ) + ).all() permitted_dataset_ids = {dp.dataset_id for dp in dataset_permission} if dataset_permission else None if user.current_role == TenantAccountRole.DATASET_OPERATOR: @@ -180,21 +192,20 @@ class DatasetService: return datasets.items, datasets.total @staticmethod - def get_process_rules(dataset_id): + def get_process_rules(dataset_id) -> ProcessRulesDict: # get the latest process rule - dataset_process_rule = ( - db.session.query(DatasetProcessRule) + dataset_process_rule = db.session.execute( + select(DatasetProcessRule) .where(DatasetProcessRule.dataset_id == dataset_id) .order_by(DatasetProcessRule.created_at.desc()) .limit(1) - .one_or_none() - ) + ).scalar_one_or_none() if dataset_process_rule: mode = dataset_process_rule.mode - rules = dataset_process_rule.rules_dict + rules = dataset_process_rule.rules_dict or {} else: - mode = DocumentService.DEFAULT_RULES["mode"] - rules = DocumentService.DEFAULT_RULES["rules"] + mode = str(DocumentService.DEFAULT_RULES["mode"]) + rules = dict(DocumentService.DEFAULT_RULES.get("rules") or {}) return {"mode": mode, "rules": rules} @staticmethod @@ -225,7 +236,7 @@ class DatasetService: summary_index_setting: dict | None = None, ): # check if dataset name already exists - if db.session.query(Dataset).filter_by(name=name, tenant_id=tenant_id).first(): + if db.session.scalar(select(Dataset).where(Dataset.name == name, Dataset.tenant_id == tenant_id).limit(1)): raise DatasetNameDuplicateError(f"Dataset with name {name} already exists.") embedding_model = None if indexing_technique == IndexTechniqueType.HIGH_QUALITY: @@ -300,17 +311,17 @@ class DatasetService: ): if rag_pipeline_dataset_create_entity.name: # check if dataset name already exists - if ( - db.session.query(Dataset) - .filter_by(name=rag_pipeline_dataset_create_entity.name, tenant_id=tenant_id) - .first() + if db.session.scalar( + select(Dataset) + .where(Dataset.name == rag_pipeline_dataset_create_entity.name, Dataset.tenant_id == tenant_id) + .limit(1) ): raise DatasetNameDuplicateError( f"Dataset with name {rag_pipeline_dataset_create_entity.name} already exists." ) else: # generate a random name as Untitled 1 2 3 ... - datasets = db.session.query(Dataset).filter_by(tenant_id=tenant_id).all() + datasets = db.session.scalars(select(Dataset).where(Dataset.tenant_id == tenant_id)).all() names = [dataset.name for dataset in datasets] rag_pipeline_dataset_create_entity.name = generate_incremental_name( names, @@ -344,7 +355,7 @@ class DatasetService: @staticmethod def get_dataset(dataset_id) -> Dataset | None: - dataset: Dataset | None = db.session.query(Dataset).filter_by(id=dataset_id).first() + dataset: Dataset | None = db.session.get(Dataset, dataset_id) return dataset @staticmethod @@ -466,14 +477,14 @@ class DatasetService: @staticmethod def _has_dataset_same_name(tenant_id: str, dataset_id: str, name: str): - dataset = ( - db.session.query(Dataset) + dataset = db.session.scalar( + select(Dataset) .where( Dataset.id != dataset_id, Dataset.name == name, Dataset.tenant_id == tenant_id, ) - .first() + .limit(1) ) return dataset is not None @@ -540,7 +551,7 @@ class DatasetService: external_knowledge_id: External knowledge identifier external_knowledge_api_id: External knowledge API identifier """ - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: external_knowledge_binding = ( session.query(ExternalKnowledgeBindings).filter_by(dataset_id=dataset_id).first() ) @@ -548,14 +559,14 @@ class DatasetService: if not external_knowledge_binding: raise ValueError("External knowledge binding not found.") - # Update binding if values have changed - if ( - external_knowledge_binding.external_knowledge_id != external_knowledge_id - or external_knowledge_binding.external_knowledge_api_id != external_knowledge_api_id - ): - external_knowledge_binding.external_knowledge_id = external_knowledge_id - external_knowledge_binding.external_knowledge_api_id = external_knowledge_api_id - db.session.add(external_knowledge_binding) + # Update binding if values have changed + if ( + external_knowledge_binding.external_knowledge_id != external_knowledge_id + or external_knowledge_binding.external_knowledge_api_id != external_knowledge_api_id + ): + external_knowledge_binding.external_knowledge_id = external_knowledge_id + external_knowledge_binding.external_knowledge_api_id = external_knowledge_api_id + session.add(external_knowledge_binding) @staticmethod def _update_internal_dataset(dataset, data, user): @@ -596,7 +607,7 @@ class DatasetService: filtered_data["icon_info"] = data.get("icon_info") # Update dataset in database - db.session.query(Dataset).filter_by(id=dataset.id).update(filtered_data) + db.session.execute(update(Dataset).where(Dataset.id == dataset.id).values(**filtered_data)) db.session.commit() # Reload dataset to get updated values @@ -631,7 +642,7 @@ class DatasetService: if dataset.runtime_mode != DatasetRuntimeMode.RAG_PIPELINE: return - pipeline = db.session.query(Pipeline).filter_by(id=dataset.pipeline_id).first() + pipeline = db.session.get(Pipeline, dataset.pipeline_id) if not pipeline: return @@ -1138,8 +1149,10 @@ class DatasetService: if dataset.permission == DatasetPermissionEnum.PARTIAL_TEAM: # For partial team permission, user needs explicit permission or be the creator if dataset.created_by != user.id: - user_permission = ( - db.session.query(DatasetPermission).filter_by(dataset_id=dataset.id, account_id=user.id).first() + user_permission = db.session.scalar( + select(DatasetPermission) + .where(DatasetPermission.dataset_id == dataset.id, DatasetPermission.account_id == user.id) + .limit(1) ) if not user_permission: logger.debug("User %s does not have permission to access dataset %s", user.id, dataset.id) @@ -1161,7 +1174,9 @@ class DatasetService: elif dataset.permission == DatasetPermissionEnum.PARTIAL_TEAM: if not any( dp.dataset_id == dataset.id - for dp in db.session.query(DatasetPermission).filter_by(account_id=user.id).all() + for dp in db.session.scalars( + select(DatasetPermission).where(DatasetPermission.account_id == user.id) + ).all() ): raise NoPermissionError("You do not have permission to access this dataset.") @@ -1175,12 +1190,11 @@ class DatasetService: @staticmethod def get_related_apps(dataset_id: str): - return ( - db.session.query(AppDatasetJoin) + return db.session.scalars( + select(AppDatasetJoin) .where(AppDatasetJoin.dataset_id == dataset_id) - .order_by(db.desc(AppDatasetJoin.created_at)) - .all() - ) + .order_by(AppDatasetJoin.created_at.desc()) + ).all() @staticmethod def update_dataset_api_status(dataset_id: str, status: bool): @@ -1195,7 +1209,7 @@ class DatasetService: db.session.commit() @staticmethod - def get_dataset_auto_disable_logs(dataset_id: str): + def get_dataset_auto_disable_logs(dataset_id: str) -> AutoDisableLogsDict: assert isinstance(current_user, Account) assert current_user.current_tenant_id is not None features = FeatureService.get_features(current_user.current_tenant_id) @@ -1396,8 +1410,8 @@ class DocumentService: @staticmethod def get_document(dataset_id: str, document_id: str | None = None) -> Document | None: if document_id: - document = ( - db.session.query(Document).where(Document.id == document_id, Document.dataset_id == dataset_id).first() + document = db.session.scalar( + select(Document).where(Document.id == document_id, Document.dataset_id == dataset_id).limit(1) ) return document else: @@ -1626,7 +1640,7 @@ class DocumentService: @staticmethod def get_document_by_id(document_id: str) -> Document | None: - document = db.session.query(Document).where(Document.id == document_id).first() + document = db.session.get(Document, document_id) return document @@ -1691,7 +1705,7 @@ class DocumentService: @staticmethod def get_document_file_detail(file_id: str): - file_detail = db.session.query(UploadFile).where(UploadFile.id == file_id).one_or_none() + file_detail = db.session.get(UploadFile, file_id) return file_detail @staticmethod @@ -1765,9 +1779,11 @@ class DocumentService: document.name = name db.session.add(document) if document.data_source_info_dict and "upload_file_id" in document.data_source_info_dict: - db.session.query(UploadFile).where( - UploadFile.id == document.data_source_info_dict["upload_file_id"] - ).update({UploadFile.name: name}) + db.session.execute( + update(UploadFile) + .where(UploadFile.id == document.data_source_info_dict["upload_file_id"]) + .values(name=name) + ) db.session.commit() @@ -1854,8 +1870,8 @@ class DocumentService: @staticmethod def get_documents_position(dataset_id): - document = ( - db.session.query(Document).filter_by(dataset_id=dataset_id).order_by(Document.position.desc()).first() + document = db.session.scalar( + select(Document).where(Document.dataset_id == dataset_id).order_by(Document.position.desc()).limit(1) ) if document: return document.position + 1 @@ -2012,28 +2028,28 @@ class DocumentService: if not knowledge_config.data_source.info_list.file_info_list: raise ValueError("File source info is required") upload_file_list = knowledge_config.data_source.info_list.file_info_list.file_ids - files = ( - db.session.query(UploadFile) - .where( - UploadFile.tenant_id == dataset.tenant_id, - UploadFile.id.in_(upload_file_list), - ) - .all() + files = list( + db.session.scalars( + select(UploadFile).where( + UploadFile.tenant_id == dataset.tenant_id, + UploadFile.id.in_(upload_file_list), + ) + ).all() ) if len(files) != len(set(upload_file_list)): raise FileNotExistsError("One or more files not found.") file_names = [file.name for file in files] - db_documents = ( - db.session.query(Document) - .where( - Document.dataset_id == dataset.id, - Document.tenant_id == current_user.current_tenant_id, - Document.data_source_type == DataSourceType.UPLOAD_FILE, - Document.enabled == True, - Document.name.in_(file_names), - ) - .all() + db_documents = list( + db.session.scalars( + select(Document).where( + Document.dataset_id == dataset.id, + Document.tenant_id == current_user.current_tenant_id, + Document.data_source_type == DataSourceType.UPLOAD_FILE, + Document.enabled == True, + Document.name.in_(file_names), + ) + ).all() ) documents_map = {document.name: document for document in db_documents} for file in files: @@ -2079,15 +2095,15 @@ class DocumentService: raise ValueError("No notion info list found.") exist_page_ids = [] exist_document = {} - documents = ( - db.session.query(Document) - .filter_by( - dataset_id=dataset.id, - tenant_id=current_user.current_tenant_id, - data_source_type=DataSourceType.NOTION_IMPORT, - enabled=True, - ) - .all() + documents = list( + db.session.scalars( + select(Document).where( + Document.dataset_id == dataset.id, + Document.tenant_id == current_user.current_tenant_id, + Document.data_source_type == DataSourceType.NOTION_IMPORT, + Document.enabled == True, + ) + ).all() ) if documents: for document in documents: @@ -2518,14 +2534,15 @@ class DocumentService: assert isinstance(current_user, Account) documents_count = ( - db.session.query(Document) - .where( - Document.completed_at.isnot(None), - Document.enabled == True, - Document.archived == False, - Document.tenant_id == current_user.current_tenant_id, + db.session.scalar( + select(func.count(Document.id)).where( + Document.completed_at.isnot(None), + Document.enabled == True, + Document.archived == False, + Document.tenant_id == current_user.current_tenant_id, + ) ) - .count() + or 0 ) return documents_count @@ -2575,10 +2592,10 @@ class DocumentService: raise ValueError("No file info list found.") upload_file_list = document_data.data_source.info_list.file_info_list.file_ids for file_id in upload_file_list: - file = ( - db.session.query(UploadFile) + file = db.session.scalar( + select(UploadFile) .where(UploadFile.tenant_id == dataset.tenant_id, UploadFile.id == file_id) - .first() + .limit(1) ) # raise error if file not found @@ -2595,8 +2612,8 @@ class DocumentService: notion_info_list = document_data.data_source.info_list.notion_info_list for notion_info in notion_info_list: workspace_id = notion_info.workspace_id - data_source_binding = ( - db.session.query(DataSourceOauthBinding) + data_source_binding = db.session.scalar( + select(DataSourceOauthBinding) .where( sa.and_( DataSourceOauthBinding.tenant_id == current_user.current_tenant_id, @@ -2605,7 +2622,7 @@ class DocumentService: DataSourceOauthBinding.source_info["workspace_id"] == f'"{workspace_id}"', ) ) - .first() + .limit(1) ) if not data_source_binding: raise ValueError("Data source binding not found.") @@ -2650,8 +2667,10 @@ class DocumentService: db.session.commit() # update document segment - db.session.query(DocumentSegment).filter_by(document_id=document.id).update( - {DocumentSegment.status: SegmentStatus.RE_SEGMENT} + db.session.execute( + update(DocumentSegment) + .where(DocumentSegment.document_id == document.id) + .values(status=SegmentStatus.RE_SEGMENT) ) db.session.commit() # trigger async task @@ -3143,10 +3162,8 @@ class SegmentService: lock_name = f"add_segment_lock_document_id_{document.id}" try: with redis_client.lock(lock_name, timeout=600): - max_position = ( - db.session.query(func.max(DocumentSegment.position)) - .where(DocumentSegment.document_id == document.id) - .scalar() + max_position = db.session.scalar( + select(func.max(DocumentSegment.position)).where(DocumentSegment.document_id == document.id) ) segment_document = DocumentSegment( tenant_id=current_user.current_tenant_id, @@ -3198,7 +3215,7 @@ class SegmentService: segment_document.status = SegmentStatus.ERROR segment_document.error = str(e) db.session.commit() - segment = db.session.query(DocumentSegment).where(DocumentSegment.id == segment_document.id).first() + segment = db.session.get(DocumentSegment, segment_document.id) return segment except LockNotOwnedError: pass @@ -3221,10 +3238,8 @@ class SegmentService: model_type=ModelType.TEXT_EMBEDDING, model=dataset.embedding_model, ) - max_position = ( - db.session.query(func.max(DocumentSegment.position)) - .where(DocumentSegment.document_id == document.id) - .scalar() + max_position = db.session.scalar( + select(func.max(DocumentSegment.position)).where(DocumentSegment.document_id == document.id) ) pre_segment_data_list = [] segment_data_list = [] @@ -3369,11 +3384,7 @@ class SegmentService: else: raise ValueError("The knowledge base index technique is not high quality!") # get the process rule - processing_rule = ( - db.session.query(DatasetProcessRule) - .where(DatasetProcessRule.id == document.dataset_process_rule_id) - .first() - ) + processing_rule = db.session.get(DatasetProcessRule, document.dataset_process_rule_id) if processing_rule: VectorService.generate_child_chunks( segment, document, dataset, embedding_model_instance, processing_rule, True @@ -3391,13 +3402,13 @@ class SegmentService: # Query existing summary from database from models.dataset import DocumentSegmentSummary - existing_summary = ( - db.session.query(DocumentSegmentSummary) + existing_summary = db.session.scalar( + select(DocumentSegmentSummary) .where( DocumentSegmentSummary.chunk_id == segment.id, DocumentSegmentSummary.dataset_id == dataset.id, ) - .first() + .limit(1) ) # Check if summary has changed @@ -3473,11 +3484,7 @@ class SegmentService: else: raise ValueError("The knowledge base index technique is not high quality!") # get the process rule - processing_rule = ( - db.session.query(DatasetProcessRule) - .where(DatasetProcessRule.id == document.dataset_process_rule_id) - .first() - ) + processing_rule = db.session.get(DatasetProcessRule, document.dataset_process_rule_id) if processing_rule: VectorService.generate_child_chunks( segment, document, dataset, embedding_model_instance, processing_rule, True @@ -3489,13 +3496,13 @@ class SegmentService: if dataset.indexing_technique == IndexTechniqueType.HIGH_QUALITY: from models.dataset import DocumentSegmentSummary - existing_summary = ( - db.session.query(DocumentSegmentSummary) + existing_summary = db.session.scalar( + select(DocumentSegmentSummary) .where( DocumentSegmentSummary.chunk_id == segment.id, DocumentSegmentSummary.dataset_id == dataset.id, ) - .first() + .limit(1) ) if args.summary is None: @@ -3561,7 +3568,7 @@ class SegmentService: segment.status = SegmentStatus.ERROR segment.error = str(e) db.session.commit() - new_segment = db.session.query(DocumentSegment).where(DocumentSegment.id == segment.id).first() + new_segment = db.session.get(DocumentSegment, segment.id) if not new_segment: raise ValueError("new_segment is not found") return new_segment @@ -3581,15 +3588,14 @@ class SegmentService: # Get child chunk IDs before parent segment is deleted child_node_ids = [] if segment.index_node_id: - child_chunks = ( - db.session.query(ChildChunk.index_node_id) - .where( - ChildChunk.segment_id == segment.id, - ChildChunk.dataset_id == dataset.id, - ) - .all() + child_node_ids = list( + db.session.scalars( + select(ChildChunk.index_node_id).where( + ChildChunk.segment_id == segment.id, + ChildChunk.dataset_id == dataset.id, + ) + ).all() ) - child_node_ids = [chunk[0] for chunk in child_chunks if chunk[0]] delete_segment_from_index_task.delay( [segment.index_node_id], dataset.id, document.id, [segment.id], child_node_ids @@ -3608,17 +3614,14 @@ class SegmentService: # Check if segment_ids is not empty to avoid WHERE false condition if not segment_ids or len(segment_ids) == 0: return - segments_info = ( - db.session.query(DocumentSegment) - .with_entities(DocumentSegment.index_node_id, DocumentSegment.id, DocumentSegment.word_count) - .where( + segments_info = db.session.execute( + select(DocumentSegment.index_node_id, DocumentSegment.id, DocumentSegment.word_count).where( DocumentSegment.id.in_(segment_ids), DocumentSegment.dataset_id == dataset.id, DocumentSegment.document_id == document.id, DocumentSegment.tenant_id == current_user.current_tenant_id, ) - .all() - ) + ).all() if not segments_info: return @@ -3630,15 +3633,16 @@ class SegmentService: # Get child chunk IDs before parent segments are deleted child_node_ids = [] if index_node_ids: - child_chunks = ( - db.session.query(ChildChunk.index_node_id) - .where( - ChildChunk.segment_id.in_(segment_db_ids), - ChildChunk.dataset_id == dataset.id, - ) - .all() - ) - child_node_ids = [chunk[0] for chunk in child_chunks if chunk[0]] + child_node_ids = [ + nid + for nid in db.session.scalars( + select(ChildChunk.index_node_id).where( + ChildChunk.segment_id.in_(segment_db_ids), + ChildChunk.dataset_id == dataset.id, + ) + ).all() + if nid + ] # Start async cleanup with both parent and child node IDs if index_node_ids or child_node_ids: @@ -3654,7 +3658,7 @@ class SegmentService: db.session.add(document) # Delete database records - db.session.query(DocumentSegment).where(DocumentSegment.id.in_(segment_ids)).delete() + db.session.execute(delete(DocumentSegment).where(DocumentSegment.id.in_(segment_ids))) db.session.commit() @classmethod @@ -3728,15 +3732,13 @@ class SegmentService: with redis_client.lock(lock_name, timeout=20): index_node_id = str(uuid.uuid4()) index_node_hash = helper.generate_text_hash(content) - max_position = ( - db.session.query(func.max(ChildChunk.position)) - .where( + max_position = db.session.scalar( + select(func.max(ChildChunk.position)).where( ChildChunk.tenant_id == current_user.current_tenant_id, ChildChunk.dataset_id == dataset.id, ChildChunk.document_id == document.id, ChildChunk.segment_id == segment.id, ) - .scalar() ) child_chunk = ChildChunk( tenant_id=current_user.current_tenant_id, @@ -3896,10 +3898,8 @@ class SegmentService: @classmethod def get_child_chunk_by_id(cls, child_chunk_id: str, tenant_id: str) -> ChildChunk | None: """Get a child chunk by its ID.""" - result = ( - db.session.query(ChildChunk) - .where(ChildChunk.id == child_chunk_id, ChildChunk.tenant_id == tenant_id) - .first() + result = db.session.scalar( + select(ChildChunk).where(ChildChunk.id == child_chunk_id, ChildChunk.tenant_id == tenant_id).limit(1) ) return result if isinstance(result, ChildChunk) else None @@ -3934,10 +3934,10 @@ class SegmentService: @classmethod def get_segment_by_id(cls, segment_id: str, tenant_id: str) -> DocumentSegment | None: """Get a segment by its ID.""" - result = ( - db.session.query(DocumentSegment) + result = db.session.scalar( + select(DocumentSegment) .where(DocumentSegment.id == segment_id, DocumentSegment.tenant_id == tenant_id) - .first() + .limit(1) ) return result if isinstance(result, DocumentSegment) else None @@ -3980,15 +3980,15 @@ class DatasetCollectionBindingService: def get_dataset_collection_binding( cls, provider_name: str, model_name: str, collection_type: str = "dataset" ) -> DatasetCollectionBinding: - dataset_collection_binding = ( - db.session.query(DatasetCollectionBinding) + dataset_collection_binding = db.session.scalar( + select(DatasetCollectionBinding) .where( DatasetCollectionBinding.provider_name == provider_name, DatasetCollectionBinding.model_name == model_name, DatasetCollectionBinding.type == collection_type, ) .order_by(DatasetCollectionBinding.created_at) - .first() + .limit(1) ) if not dataset_collection_binding: @@ -4006,13 +4006,13 @@ class DatasetCollectionBindingService: def get_dataset_collection_binding_by_id_and_type( cls, collection_binding_id: str, collection_type: str = "dataset" ) -> DatasetCollectionBinding: - dataset_collection_binding = ( - db.session.query(DatasetCollectionBinding) + dataset_collection_binding = db.session.scalar( + select(DatasetCollectionBinding) .where( DatasetCollectionBinding.id == collection_binding_id, DatasetCollectionBinding.type == collection_type ) .order_by(DatasetCollectionBinding.created_at) - .first() + .limit(1) ) if not dataset_collection_binding: raise ValueError("Dataset collection binding not found") @@ -4034,7 +4034,7 @@ class DatasetPermissionService: @classmethod def update_partial_member_list(cls, tenant_id, dataset_id, user_list): try: - db.session.query(DatasetPermission).where(DatasetPermission.dataset_id == dataset_id).delete() + db.session.execute(delete(DatasetPermission).where(DatasetPermission.dataset_id == dataset_id)) permissions = [] for user in user_list: permission = DatasetPermission( @@ -4070,7 +4070,7 @@ class DatasetPermissionService: @classmethod def clear_partial_member_list(cls, dataset_id): try: - db.session.query(DatasetPermission).where(DatasetPermission.dataset_id == dataset_id).delete() + db.session.execute(delete(DatasetPermission).where(DatasetPermission.dataset_id == dataset_id)) db.session.commit() except Exception as e: db.session.rollback() diff --git a/api/services/datasource_provider_service.py b/api/services/datasource_provider_service.py index 06f83a18f7..faa978afdc 100644 --- a/api/services/datasource_provider_service.py +++ b/api/services/datasource_provider_service.py @@ -4,6 +4,7 @@ from collections.abc import Mapping from typing import Any from graphon.model_runtime.entities.provider_entities import FormType +from sqlalchemy import func, select from sqlalchemy.orm import Session from configs import dify_config @@ -367,16 +368,16 @@ class DatasourceProviderService: check if tenant oauth params is enabled """ return ( - db.session.query(DatasourceOauthTenantParamConfig) - .filter_by( - tenant_id=tenant_id, - provider=datasource_provider_id.provider_name, - plugin_id=datasource_provider_id.plugin_id, - enabled=True, + db.session.scalar( + select(func.count(DatasourceOauthTenantParamConfig.id)).where( + DatasourceOauthTenantParamConfig.tenant_id == tenant_id, + DatasourceOauthTenantParamConfig.provider == datasource_provider_id.provider_name, + DatasourceOauthTenantParamConfig.plugin_id == datasource_provider_id.plugin_id, + DatasourceOauthTenantParamConfig.enabled == True, + ) ) - .count() - > 0 - ) + or 0 + ) > 0 def get_tenant_oauth_client( self, tenant_id: str, datasource_provider_id: DatasourceProviderID, mask: bool = False @@ -384,14 +385,14 @@ class DatasourceProviderService: """ get tenant oauth client """ - tenant_oauth_client_params = ( - db.session.query(DatasourceOauthTenantParamConfig) - .filter_by( - tenant_id=tenant_id, - provider=datasource_provider_id.provider_name, - plugin_id=datasource_provider_id.plugin_id, + tenant_oauth_client_params = db.session.scalar( + select(DatasourceOauthTenantParamConfig) + .where( + DatasourceOauthTenantParamConfig.tenant_id == tenant_id, + DatasourceOauthTenantParamConfig.provider == datasource_provider_id.provider_name, + DatasourceOauthTenantParamConfig.plugin_id == datasource_provider_id.plugin_id, ) - .first() + .limit(1) ) if tenant_oauth_client_params: encrypter, _ = self.get_oauth_encrypter(tenant_id, datasource_provider_id) @@ -707,24 +708,27 @@ class DatasourceProviderService: :return: """ # Get all provider configurations of the current workspace - datasource_providers: list[DatasourceProvider] = ( - db.session.query(DatasourceProvider) + datasource_providers: list[DatasourceProvider] = list( + db.session.scalars( + select(DatasourceProvider).where( + DatasourceProvider.tenant_id == tenant_id, + DatasourceProvider.provider == provider, + DatasourceProvider.plugin_id == plugin_id, + ) + ).all() + ) + if not datasource_providers: + return [] + copy_credentials_list = [] + default_provider = db.session.execute( + select(DatasourceProvider.id) .where( DatasourceProvider.tenant_id == tenant_id, DatasourceProvider.provider == provider, DatasourceProvider.plugin_id == plugin_id, ) - .all() - ) - if not datasource_providers: - return [] - copy_credentials_list = [] - default_provider = ( - db.session.query(DatasourceProvider.id) - .filter_by(tenant_id=tenant_id, provider=provider, plugin_id=plugin_id) .order_by(DatasourceProvider.is_default.desc(), DatasourceProvider.created_at.asc()) - .first() - ) + ).first() default_provider_id = default_provider.id if default_provider else None for datasource_provider in datasource_providers: encrypted_credentials = datasource_provider.encrypted_credentials @@ -880,14 +884,14 @@ class DatasourceProviderService: :return: """ # Get all provider configurations of the current workspace - datasource_providers: list[DatasourceProvider] = ( - db.session.query(DatasourceProvider) - .where( - DatasourceProvider.tenant_id == tenant_id, - DatasourceProvider.provider == provider, - DatasourceProvider.plugin_id == plugin_id, - ) - .all() + datasource_providers: list[DatasourceProvider] = list( + db.session.scalars( + select(DatasourceProvider).where( + DatasourceProvider.tenant_id == tenant_id, + DatasourceProvider.provider == provider, + DatasourceProvider.plugin_id == plugin_id, + ) + ).all() ) if not datasource_providers: return [] @@ -987,10 +991,15 @@ class DatasourceProviderService: :param plugin_id: plugin id :return: """ - datasource_provider = ( - db.session.query(DatasourceProvider) - .filter_by(tenant_id=tenant_id, id=auth_id, provider=provider, plugin_id=plugin_id) - .first() + datasource_provider = db.session.scalar( + select(DatasourceProvider) + .where( + DatasourceProvider.tenant_id == tenant_id, + DatasourceProvider.id == auth_id, + DatasourceProvider.provider == provider, + DatasourceProvider.plugin_id == plugin_id, + ) + .limit(1) ) if datasource_provider: db.session.delete(datasource_provider) diff --git a/api/services/end_user_service.py b/api/services/end_user_service.py index 326f46780d..29ada270ec 100644 --- a/api/services/end_user_service.py +++ b/api/services/end_user_service.py @@ -1,7 +1,7 @@ import logging from collections.abc import Mapping -from sqlalchemy import case +from sqlalchemy import case, select from sqlalchemy.orm import Session from core.app.entities.app_invoke_entities import InvokeFrom @@ -25,14 +25,14 @@ class EndUserService: """ with Session(db.engine, expire_on_commit=False) as session: - return ( - session.query(EndUser) + return session.scalar( + select(EndUser) .where( EndUser.id == end_user_id, EndUser.tenant_id == tenant_id, EndUser.app_id == app_id, ) - .first() + .limit(1) ) @classmethod @@ -57,8 +57,8 @@ class EndUserService: with Session(db.engine, expire_on_commit=False) as session: # Query with ORDER BY to prioritize exact type matches while maintaining backward compatibility # This single query approach is more efficient than separate queries - end_user = ( - session.query(EndUser) + end_user = session.scalar( + select(EndUser) .where( EndUser.tenant_id == tenant_id, EndUser.app_id == app_id, @@ -68,7 +68,7 @@ class EndUserService: # Prioritize records with matching type (0 = match, 1 = no match) case((EndUser.type == type, 0), else_=1) ) - .first() + .limit(1) ) if end_user: @@ -137,15 +137,15 @@ class EndUserService: with Session(db.engine, expire_on_commit=False) as session: # Fetch existing end users for all target apps in a single query - existing_end_users: list[EndUser] = ( - session.query(EndUser) - .where( - EndUser.tenant_id == tenant_id, - EndUser.app_id.in_(unique_app_ids), - EndUser.session_id == user_id, - EndUser.type == type, - ) - .all() + existing_end_users: list[EndUser] = list( + session.scalars( + select(EndUser).where( + EndUser.tenant_id == tenant_id, + EndUser.app_id.in_(unique_app_ids), + EndUser.session_id == user_id, + EndUser.type == type, + ) + ).all() ) found_app_ids: set[str] = set() diff --git a/api/services/enterprise/plugin_manager_service.py b/api/services/enterprise/plugin_manager_service.py index d4be36305e..23571f2d7d 100644 --- a/api/services/enterprise/plugin_manager_service.py +++ b/api/services/enterprise/plugin_manager_service.py @@ -1,23 +1,15 @@ -import enum import logging from pydantic import BaseModel from configs import dify_config +from core.entities import PluginCredentialType from services.enterprise.base import EnterprisePluginManagerRequest from services.errors.base import BaseServiceError logger = logging.getLogger(__name__) -class PluginCredentialType(enum.Enum): - MODEL = 0 # must be 0 for API contract compatibility - TOOL = 1 # must be 1 for API contract compatibility - - def to_number(self): - return self.value - - class CheckCredentialPolicyComplianceRequest(BaseModel): dify_credential_id: str provider: str diff --git a/api/services/entities/auth_entities.py b/api/services/entities/auth_entities.py new file mode 100644 index 0000000000..6b720a4607 --- /dev/null +++ b/api/services/entities/auth_entities.py @@ -0,0 +1,31 @@ +from pydantic import BaseModel, Field, field_validator + +from libs.helper import EmailStr +from libs.password import valid_password + + +class LoginPayloadBase(BaseModel): + email: EmailStr + password: str + + +class ForgotPasswordSendPayload(BaseModel): + email: EmailStr + language: str | None = None + + +class ForgotPasswordCheckPayload(BaseModel): + email: EmailStr + code: str + token: str = Field(min_length=1) + + +class ForgotPasswordResetPayload(BaseModel): + token: str = Field(min_length=1) + new_password: str + password_confirm: str + + @field_validator("new_password", "password_confirm") + @classmethod + def validate_password(cls, value: str) -> str: + return valid_password(value) diff --git a/api/services/entities/knowledge_entities/knowledge_entities.py b/api/services/entities/knowledge_entities/knowledge_entities.py index 66309f0e59..cb38104e8c 100644 --- a/api/services/entities/knowledge_entities/knowledge_entities.py +++ b/api/services/entities/knowledge_entities/knowledge_entities.py @@ -1,17 +1,12 @@ -from enum import StrEnum from typing import Literal from pydantic import BaseModel, field_validator +from core.rag.entities import Rule from core.rag.index_processor.constant.index_type import IndexStructureType from core.rag.retrieval.retrieval_methods import RetrievalMethod -class ParentMode(StrEnum): - FULL_DOC = "full-doc" - PARAGRAPH = "paragraph" - - class NotionIcon(BaseModel): type: str url: str | None = None @@ -53,24 +48,6 @@ class DataSource(BaseModel): info_list: InfoList -class PreProcessingRule(BaseModel): - id: str - enabled: bool - - -class Segmentation(BaseModel): - separator: str = "\n" - max_tokens: int - chunk_overlap: int = 0 - - -class Rule(BaseModel): - pre_processing_rules: list[PreProcessingRule] | None = None - segmentation: Segmentation | None = None - parent_mode: Literal["full-doc", "paragraph"] | None = None - subchunk_segmentation: Segmentation | None = None - - class ProcessRule(BaseModel): mode: Literal["automatic", "custom", "hierarchical"] rules: Rule | None = None diff --git a/api/services/entities/knowledge_entities/rag_pipeline_entities.py b/api/services/entities/knowledge_entities/rag_pipeline_entities.py index 041ae4edba..a360fd2854 100644 --- a/api/services/entities/knowledge_entities/rag_pipeline_entities.py +++ b/api/services/entities/knowledge_entities/rag_pipeline_entities.py @@ -2,6 +2,7 @@ from typing import Literal from pydantic import BaseModel, field_validator +from core.rag.entities import KeywordSetting, VectorSetting from core.rag.retrieval.retrieval_methods import RetrievalMethod @@ -36,24 +37,6 @@ class RerankingModelConfig(BaseModel): reranking_model_name: str | None = "" -class VectorSetting(BaseModel): - """ - Vector Setting. - """ - - vector_weight: float - embedding_provider_name: str - embedding_model_name: str - - -class KeywordSetting(BaseModel): - """ - Keyword Setting. - """ - - keyword_weight: float - - class WeightedScoreConfig(BaseModel): """ Weighted score Config. @@ -63,23 +46,6 @@ class WeightedScoreConfig(BaseModel): keyword_setting: KeywordSetting | None -class EmbeddingSetting(BaseModel): - """ - Embedding Setting. - """ - - embedding_provider_name: str - embedding_model_name: str - - -class EconomySetting(BaseModel): - """ - Economy Setting. - """ - - keyword_number: int - - class RetrievalSetting(BaseModel): """ Retrieval Setting. @@ -95,16 +61,6 @@ class RetrievalSetting(BaseModel): weights: WeightedScoreConfig | None = None -class IndexMethod(BaseModel): - """ - Knowledge Index Setting. - """ - - indexing_technique: Literal["high_quality", "economy"] - embedding_setting: EmbeddingSetting - economy_setting: EconomySetting - - class KnowledgeConfiguration(BaseModel): """ Knowledge Base Configuration. diff --git a/api/services/external_knowledge_service.py b/api/services/external_knowledge_service.py index 9a522ece52..d30ec940f5 100644 --- a/api/services/external_knowledge_service.py +++ b/api/services/external_knowledge_service.py @@ -5,11 +5,11 @@ from urllib.parse import urlparse import httpx from graphon.nodes.http_request.exc import InvalidHttpMethodError -from sqlalchemy import select +from sqlalchemy import func, select from constants import HIDDEN_VALUE from core.helper import ssrf_proxy -from core.rag.entities.metadata_entities import MetadataCondition +from core.rag.entities import MetadataFilteringCondition from extensions.ext_database import db from libs.datetime_utils import naive_utc_now from models.dataset import ( @@ -103,8 +103,10 @@ class ExternalDatasetService: @staticmethod def get_external_knowledge_api(external_knowledge_api_id: str, tenant_id: str) -> ExternalKnowledgeApis: - external_knowledge_api: ExternalKnowledgeApis | None = ( - db.session.query(ExternalKnowledgeApis).filter_by(id=external_knowledge_api_id, tenant_id=tenant_id).first() + external_knowledge_api: ExternalKnowledgeApis | None = db.session.scalar( + select(ExternalKnowledgeApis) + .where(ExternalKnowledgeApis.id == external_knowledge_api_id, ExternalKnowledgeApis.tenant_id == tenant_id) + .limit(1) ) if external_knowledge_api is None: raise ValueError("api template not found") @@ -112,8 +114,10 @@ class ExternalDatasetService: @staticmethod def update_external_knowledge_api(tenant_id, user_id, external_knowledge_api_id, args) -> ExternalKnowledgeApis: - external_knowledge_api: ExternalKnowledgeApis | None = ( - db.session.query(ExternalKnowledgeApis).filter_by(id=external_knowledge_api_id, tenant_id=tenant_id).first() + external_knowledge_api: ExternalKnowledgeApis | None = db.session.scalar( + select(ExternalKnowledgeApis) + .where(ExternalKnowledgeApis.id == external_knowledge_api_id, ExternalKnowledgeApis.tenant_id == tenant_id) + .limit(1) ) if external_knowledge_api is None: raise ValueError("api template not found") @@ -132,8 +136,10 @@ class ExternalDatasetService: @staticmethod def delete_external_knowledge_api(tenant_id: str, external_knowledge_api_id: str): - external_knowledge_api = ( - db.session.query(ExternalKnowledgeApis).filter_by(id=external_knowledge_api_id, tenant_id=tenant_id).first() + external_knowledge_api = db.session.scalar( + select(ExternalKnowledgeApis) + .where(ExternalKnowledgeApis.id == external_knowledge_api_id, ExternalKnowledgeApis.tenant_id == tenant_id) + .limit(1) ) if external_knowledge_api is None: raise ValueError("api template not found") @@ -144,9 +150,12 @@ class ExternalDatasetService: @staticmethod def external_knowledge_api_use_check(external_knowledge_api_id: str) -> tuple[bool, int]: count = ( - db.session.query(ExternalKnowledgeBindings) - .filter_by(external_knowledge_api_id=external_knowledge_api_id) - .count() + db.session.scalar( + select(func.count(ExternalKnowledgeBindings.id)).where( + ExternalKnowledgeBindings.external_knowledge_api_id == external_knowledge_api_id + ) + ) + or 0 ) if count > 0: return True, count @@ -154,8 +163,10 @@ class ExternalDatasetService: @staticmethod def get_external_knowledge_binding_with_dataset_id(tenant_id: str, dataset_id: str) -> ExternalKnowledgeBindings: - external_knowledge_binding: ExternalKnowledgeBindings | None = ( - db.session.query(ExternalKnowledgeBindings).filter_by(dataset_id=dataset_id, tenant_id=tenant_id).first() + external_knowledge_binding: ExternalKnowledgeBindings | None = db.session.scalar( + select(ExternalKnowledgeBindings) + .where(ExternalKnowledgeBindings.dataset_id == dataset_id, ExternalKnowledgeBindings.tenant_id == tenant_id) + .limit(1) ) if not external_knowledge_binding: raise ValueError("external knowledge binding not found") @@ -163,8 +174,10 @@ class ExternalDatasetService: @staticmethod def document_create_args_validate(tenant_id: str, external_knowledge_api_id: str, process_parameter: dict): - external_knowledge_api = ( - db.session.query(ExternalKnowledgeApis).filter_by(id=external_knowledge_api_id, tenant_id=tenant_id).first() + external_knowledge_api = db.session.scalar( + select(ExternalKnowledgeApis) + .where(ExternalKnowledgeApis.id == external_knowledge_api_id, ExternalKnowledgeApis.tenant_id == tenant_id) + .limit(1) ) if external_knowledge_api is None or external_knowledge_api.settings is None: raise ValueError("api template not found") @@ -238,12 +251,17 @@ class ExternalDatasetService: @staticmethod def create_external_dataset(tenant_id: str, user_id: str, args: dict) -> Dataset: # check if dataset name already exists - if db.session.query(Dataset).filter_by(name=args.get("name"), tenant_id=tenant_id).first(): + if db.session.scalar( + select(Dataset).where(Dataset.name == args.get("name"), Dataset.tenant_id == tenant_id).limit(1) + ): raise DatasetNameDuplicateError(f"Dataset with name {args.get('name')} already exists.") - external_knowledge_api = ( - db.session.query(ExternalKnowledgeApis) - .filter_by(id=args.get("external_knowledge_api_id"), tenant_id=tenant_id) - .first() + external_knowledge_api = db.session.scalar( + select(ExternalKnowledgeApis) + .where( + ExternalKnowledgeApis.id == args.get("external_knowledge_api_id"), + ExternalKnowledgeApis.tenant_id == tenant_id, + ) + .limit(1) ) if external_knowledge_api is None: @@ -284,18 +302,20 @@ class ExternalDatasetService: dataset_id: str, query: str, external_retrieval_parameters: dict, - metadata_condition: MetadataCondition | None = None, + metadata_condition: MetadataFilteringCondition | None = None, ): - external_knowledge_binding = ( - db.session.query(ExternalKnowledgeBindings).filter_by(dataset_id=dataset_id, tenant_id=tenant_id).first() + external_knowledge_binding = db.session.scalar( + select(ExternalKnowledgeBindings) + .where(ExternalKnowledgeBindings.dataset_id == dataset_id, ExternalKnowledgeBindings.tenant_id == tenant_id) + .limit(1) ) if not external_knowledge_binding: raise ValueError("external knowledge binding not found") - external_knowledge_api = ( - db.session.query(ExternalKnowledgeApis) - .filter_by(id=external_knowledge_binding.external_knowledge_api_id) - .first() + external_knowledge_api = db.session.scalar( + select(ExternalKnowledgeApis) + .where(ExternalKnowledgeApis.id == external_knowledge_binding.external_knowledge_api_id) + .limit(1) ) if external_knowledge_api is None or external_knowledge_api.settings is None: raise ValueError("external api template not found") diff --git a/api/services/hit_testing_service.py b/api/services/hit_testing_service.py index 82e0b0f8b1..7e0100212a 100644 --- a/api/services/hit_testing_service.py +++ b/api/services/hit_testing_service.py @@ -1,7 +1,7 @@ import json import logging import time -from typing import Any +from typing import Any, TypedDict from graphon.model_runtime.entities import LLMMode @@ -18,6 +18,16 @@ from models.enums import CreatorUserRole, DatasetQuerySource logger = logging.getLogger(__name__) + +class QueryDict(TypedDict): + content: str + + +class RetrieveResponseDict(TypedDict): + query: QueryDict + records: list[dict[str, Any]] + + default_retrieval_model = { "search_method": RetrievalMethod.SEMANTIC_SEARCH, "reranking_enable": False, @@ -34,7 +44,7 @@ class HitTestingService: dataset: Dataset, query: str, account: Account, - retrieval_model: Any, # FIXME drop this any + retrieval_model: dict | None, external_retrieval_model: dict, attachment_ids: list | None = None, limit: int = 10, @@ -44,12 +54,13 @@ class HitTestingService: # get retrieval model , if the model is not setting , using default if not retrieval_model: retrieval_model = dataset.retrieval_model or default_retrieval_model + assert isinstance(retrieval_model, dict) document_ids_filter = None metadata_filtering_conditions = retrieval_model.get("metadata_filtering_conditions", {}) if metadata_filtering_conditions and query: dataset_retrieval = DatasetRetrieval() - from core.app.app_config.entities import MetadataFilteringCondition + from core.rag.entities import MetadataFilteringCondition metadata_filtering_conditions = MetadataFilteringCondition.model_validate(metadata_filtering_conditions) @@ -150,7 +161,7 @@ class HitTestingService: return dict(cls.compact_external_retrieve_response(dataset, query, all_documents)) @classmethod - def compact_retrieve_response(cls, query: str, documents: list[Document]) -> dict[Any, Any]: + def compact_retrieve_response(cls, query: str, documents: list[Document]) -> RetrieveResponseDict: records = RetrievalService.format_retrieval_documents(documents) return { @@ -161,7 +172,7 @@ class HitTestingService: } @classmethod - def compact_external_retrieve_response(cls, dataset: Dataset, query: str, documents: list) -> dict[Any, Any]: + def compact_external_retrieve_response(cls, dataset: Dataset, query: str, documents: list) -> RetrieveResponseDict: records = [] if dataset.provider == "external": for document in documents: diff --git a/api/services/metadata_service.py b/api/services/metadata_service.py index 12729278cc..672f309bac 100644 --- a/api/services/metadata_service.py +++ b/api/services/metadata_service.py @@ -1,6 +1,8 @@ import copy import logging +from sqlalchemy import delete, func, select + from core.rag.index_processor.constant.built_in_field import BuiltInField, MetadataDataSource from extensions.ext_database import db from extensions.ext_redis import redis_client @@ -25,10 +27,14 @@ class MetadataService: raise ValueError("Metadata name cannot exceed 255 characters.") current_user, current_tenant_id = current_account_with_tenant() # check if metadata name already exists - if ( - db.session.query(DatasetMetadata) - .filter_by(tenant_id=current_tenant_id, dataset_id=dataset_id, name=metadata_args.name) - .first() + if db.session.scalar( + select(DatasetMetadata) + .where( + DatasetMetadata.tenant_id == current_tenant_id, + DatasetMetadata.dataset_id == dataset_id, + DatasetMetadata.name == metadata_args.name, + ) + .limit(1) ): raise ValueError("Metadata name already exists.") for field in BuiltInField: @@ -54,10 +60,14 @@ class MetadataService: lock_key = f"dataset_metadata_lock_{dataset_id}" # check if metadata name already exists current_user, current_tenant_id = current_account_with_tenant() - if ( - db.session.query(DatasetMetadata) - .filter_by(tenant_id=current_tenant_id, dataset_id=dataset_id, name=name) - .first() + if db.session.scalar( + select(DatasetMetadata) + .where( + DatasetMetadata.tenant_id == current_tenant_id, + DatasetMetadata.dataset_id == dataset_id, + DatasetMetadata.name == name, + ) + .limit(1) ): raise ValueError("Metadata name already exists.") for field in BuiltInField: @@ -65,7 +75,11 @@ class MetadataService: raise ValueError("Metadata name already exists in Built-in fields.") try: MetadataService.knowledge_base_metadata_lock_check(dataset_id, None) - metadata = db.session.query(DatasetMetadata).filter_by(id=metadata_id, dataset_id=dataset_id).first() + metadata = db.session.scalar( + select(DatasetMetadata) + .where(DatasetMetadata.id == metadata_id, DatasetMetadata.dataset_id == dataset_id) + .limit(1) + ) if metadata is None: raise ValueError("Metadata not found.") old_name = metadata.name @@ -74,9 +88,9 @@ class MetadataService: metadata.updated_at = naive_utc_now() # update related documents - dataset_metadata_bindings = ( - db.session.query(DatasetMetadataBinding).filter_by(metadata_id=metadata_id).all() - ) + dataset_metadata_bindings = db.session.scalars( + select(DatasetMetadataBinding).where(DatasetMetadataBinding.metadata_id == metadata_id) + ).all() if dataset_metadata_bindings: document_ids = [binding.document_id for binding in dataset_metadata_bindings] documents = DocumentService.get_document_by_ids(document_ids) @@ -101,15 +115,19 @@ class MetadataService: lock_key = f"dataset_metadata_lock_{dataset_id}" try: MetadataService.knowledge_base_metadata_lock_check(dataset_id, None) - metadata = db.session.query(DatasetMetadata).filter_by(id=metadata_id, dataset_id=dataset_id).first() + metadata = db.session.scalar( + select(DatasetMetadata) + .where(DatasetMetadata.id == metadata_id, DatasetMetadata.dataset_id == dataset_id) + .limit(1) + ) if metadata is None: raise ValueError("Metadata not found.") db.session.delete(metadata) # deal related documents - dataset_metadata_bindings = ( - db.session.query(DatasetMetadataBinding).filter_by(metadata_id=metadata_id).all() - ) + dataset_metadata_bindings = db.session.scalars( + select(DatasetMetadataBinding).where(DatasetMetadataBinding.metadata_id == metadata_id) + ).all() if dataset_metadata_bindings: document_ids = [binding.document_id for binding in dataset_metadata_bindings] documents = DocumentService.get_document_by_ids(document_ids) @@ -224,16 +242,23 @@ class MetadataService: # deal metadata binding (in the same transaction as the doc_metadata update) if not operation.partial_update: - db.session.query(DatasetMetadataBinding).filter_by(document_id=operation.document_id).delete() + db.session.execute( + delete(DatasetMetadataBinding).where( + DatasetMetadataBinding.document_id == operation.document_id + ) + ) current_user, current_tenant_id = current_account_with_tenant() for metadata_value in operation.metadata_list: # check if binding already exists if operation.partial_update: - existing_binding = ( - db.session.query(DatasetMetadataBinding) - .filter_by(document_id=operation.document_id, metadata_id=metadata_value.id) - .first() + existing_binding = db.session.scalar( + select(DatasetMetadataBinding) + .where( + DatasetMetadataBinding.document_id == operation.document_id, + DatasetMetadataBinding.metadata_id == metadata_value.id, + ) + .limit(1) ) if existing_binding: continue @@ -275,9 +300,13 @@ class MetadataService: "id": item.get("id"), "name": item.get("name"), "type": item.get("type"), - "count": db.session.query(DatasetMetadataBinding) - .filter_by(metadata_id=item.get("id"), dataset_id=dataset.id) - .count(), + "count": db.session.scalar( + select(func.count(DatasetMetadataBinding.id)).where( + DatasetMetadataBinding.metadata_id == item.get("id"), + DatasetMetadataBinding.dataset_id == dataset.id, + ) + ) + or 0, } for item in dataset.doc_metadata or [] if item.get("id") != "built-in" diff --git a/api/services/model_load_balancing_service.py b/api/services/model_load_balancing_service.py index bc0bfd215c..3cce83a975 100644 --- a/api/services/model_load_balancing_service.py +++ b/api/services/model_load_balancing_service.py @@ -1,6 +1,6 @@ import json import logging -from typing import Any, Union +from typing import Any, TypedDict, Union from graphon.model_runtime.entities.model_entities import ModelType from graphon.model_runtime.entities.provider_entities import ( @@ -25,6 +25,23 @@ from models.provider import LoadBalancingModelConfig, ProviderCredential, Provid logger = logging.getLogger(__name__) +class LoadBalancingConfigDetailDict(TypedDict): + id: str + name: str + credentials: dict[str, Any] + enabled: bool + + +class LoadBalancingConfigSummaryDict(TypedDict): + id: str + name: str + credentials: dict[str, Any] + credential_id: str | None + enabled: bool + in_cooldown: bool + ttl: int + + class ModelLoadBalancingService: @staticmethod def _get_provider_manager(tenant_id: str) -> ProviderManager: @@ -74,7 +91,7 @@ class ModelLoadBalancingService: def get_load_balancing_configs( self, tenant_id: str, provider: str, model: str, model_type: str, config_from: str = "" - ) -> tuple[bool, list[dict]]: + ) -> tuple[bool, list[LoadBalancingConfigSummaryDict]]: """ Get load balancing configurations. :param tenant_id: workspace id @@ -156,7 +173,7 @@ class ModelLoadBalancingService: decoding_rsa_key, decoding_cipher_rsa = encrypter.get_decrypt_decoding(tenant_id) # fetch status and ttl for each config - datas = [] + datas: list[LoadBalancingConfigSummaryDict] = [] for load_balancing_config in load_balancing_configs: in_cooldown, ttl = LBModelManager.get_config_in_cooldown_and_ttl( tenant_id=tenant_id, @@ -214,7 +231,7 @@ class ModelLoadBalancingService: def get_load_balancing_config( self, tenant_id: str, provider: str, model: str, model_type: str, config_id: str - ) -> dict | None: + ) -> LoadBalancingConfigDetailDict | None: """ Get load balancing configuration. :param tenant_id: workspace id @@ -267,12 +284,13 @@ class ModelLoadBalancingService: credentials=credentials, credential_form_schemas=credential_schemas.credential_form_schemas ) - return { + result: LoadBalancingConfigDetailDict = { "id": load_balancing_model_config.id, "name": load_balancing_model_config.name, "credentials": credentials, "enabled": load_balancing_model_config.enabled, } + return result def _init_inherit_config( self, tenant_id: str, provider: str, model: str, model_type: ModelType diff --git a/api/services/oauth_server.py b/api/services/oauth_server.py index b05b43d76e..22648070f0 100644 --- a/api/services/oauth_server.py +++ b/api/services/oauth_server.py @@ -2,7 +2,7 @@ import enum import uuid from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest from extensions.ext_database import db @@ -29,7 +29,7 @@ class OAuthServerService: def get_oauth_provider_app(client_id: str) -> OAuthProviderApp | None: query = select(OAuthProviderApp).where(OAuthProviderApp.client_id == client_id) - with Session(db.engine) as session: + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: return session.execute(query).scalar_one_or_none() @staticmethod diff --git a/api/services/plugin/plugin_migration.py b/api/services/plugin/plugin_migration.py index 442ccef1da..d6f6ee8086 100644 --- a/api/services/plugin/plugin_migration.py +++ b/api/services/plugin/plugin_migration.py @@ -5,7 +5,7 @@ import time from collections.abc import Mapping, Sequence from concurrent.futures import ThreadPoolExecutor from pathlib import Path -from typing import Any, TypedDict +from typing import TypedDict from uuid import uuid4 import click @@ -42,6 +42,16 @@ class _TenantPluginRecord(TypedDict): _tenant_plugin_adapter: TypeAdapter[_TenantPluginRecord] = TypeAdapter(_TenantPluginRecord) +class ExtractedPluginsDict(TypedDict): + plugins: dict[str, str] + plugin_not_exist: list[str] + + +class PluginInstallResultDict(TypedDict): + success: list[str] + failed: list[str] + + class PluginMigration: @classmethod def extract_plugins(cls, filepath: str, workers: int): @@ -310,7 +320,7 @@ class PluginMigration: Path(output_file).write_text(json.dumps(cls.extract_unique_plugins(extracted_plugins))) @classmethod - def extract_unique_plugins(cls, extracted_plugins: str) -> Mapping[str, Any]: + def extract_unique_plugins(cls, extracted_plugins: str) -> ExtractedPluginsDict: plugins: dict[str, str] = {} plugin_ids = [] plugin_not_exist = [] @@ -524,7 +534,7 @@ class PluginMigration: @classmethod def handle_plugin_instance_install( cls, tenant_id: str, plugin_identifiers_map: Mapping[str, str] - ) -> Mapping[str, Any]: + ) -> PluginInstallResultDict: """ Install plugins for a tenant. """ diff --git a/api/services/plugin/plugin_parameter_service.py b/api/services/plugin/plugin_parameter_service.py index 40565c56ed..786c09b44e 100644 --- a/api/services/plugin/plugin_parameter_service.py +++ b/api/services/plugin/plugin_parameter_service.py @@ -1,6 +1,7 @@ from collections.abc import Mapping, Sequence from typing import Any, Literal +from sqlalchemy import select from sqlalchemy.orm import Session from core.plugin.entities.parameters import PluginParameterOption @@ -56,24 +57,24 @@ class PluginParameterService: # fetch credentials from db with Session(db.engine) as session: if credential_id: - db_record = ( - session.query(BuiltinToolProvider) + db_record = session.scalar( + select(BuiltinToolProvider) .where( BuiltinToolProvider.tenant_id == tenant_id, BuiltinToolProvider.provider == provider, BuiltinToolProvider.id == credential_id, ) - .first() + .limit(1) ) else: - db_record = ( - session.query(BuiltinToolProvider) + db_record = session.scalar( + select(BuiltinToolProvider) .where( BuiltinToolProvider.tenant_id == tenant_id, BuiltinToolProvider.provider == provider, ) .order_by(BuiltinToolProvider.is_default.desc(), BuiltinToolProvider.created_at.asc()) - .first() + .limit(1) ) if db_record is None: diff --git a/api/services/rag_pipeline/rag_pipeline.py b/api/services/rag_pipeline/rag_pipeline.py index 50f34d5a8a..b330e1a46a 100644 --- a/api/services/rag_pipeline/rag_pipeline.py +++ b/api/services/rag_pipeline/rag_pipeline.py @@ -38,11 +38,7 @@ from core.datasource.online_document.online_document_plugin import OnlineDocumen from core.datasource.online_drive.online_drive_plugin import OnlineDriveDatasourcePlugin from core.datasource.website_crawl.website_crawl_plugin import WebsiteCrawlDatasourcePlugin from core.helper import marketplace -from core.rag.entities.event import ( - DatasourceCompletedEvent, - DatasourceErrorEvent, - DatasourceProcessingEvent, -) +from core.rag.entities import DatasourceCompletedEvent, DatasourceErrorEvent, DatasourceProcessingEvent from core.repositories.factory import DifyCoreRepositoryFactory, OrderConfig from core.repositories.sqlalchemy_workflow_node_execution_repository import SQLAlchemyWorkflowNodeExecutionRepository from core.workflow.node_factory import LATEST_VERSION, get_node_type_classes_mapping @@ -156,27 +152,27 @@ class RagPipelineService: :param template_id: template id :param template_info: template info """ - customized_template: PipelineCustomizedTemplate | None = ( - db.session.query(PipelineCustomizedTemplate) + customized_template: PipelineCustomizedTemplate | None = db.session.scalar( + select(PipelineCustomizedTemplate) .where( PipelineCustomizedTemplate.id == template_id, PipelineCustomizedTemplate.tenant_id == current_user.current_tenant_id, ) - .first() + .limit(1) ) if not customized_template: raise ValueError("Customized pipeline template not found.") # check template name is exist template_name = template_info.name if template_name: - template = ( - db.session.query(PipelineCustomizedTemplate) + template = db.session.scalar( + select(PipelineCustomizedTemplate) .where( PipelineCustomizedTemplate.name == template_name, PipelineCustomizedTemplate.tenant_id == current_user.current_tenant_id, PipelineCustomizedTemplate.id != template_id, ) - .first() + .limit(1) ) if template: raise ValueError("Template name is already exists") @@ -192,13 +188,13 @@ class RagPipelineService: """ Delete customized pipeline template. """ - customized_template: PipelineCustomizedTemplate | None = ( - db.session.query(PipelineCustomizedTemplate) + customized_template: PipelineCustomizedTemplate | None = db.session.scalar( + select(PipelineCustomizedTemplate) .where( PipelineCustomizedTemplate.id == template_id, PipelineCustomizedTemplate.tenant_id == current_user.current_tenant_id, ) - .first() + .limit(1) ) if not customized_template: raise ValueError("Customized pipeline template not found.") @@ -210,14 +206,14 @@ class RagPipelineService: Get draft workflow """ # fetch draft workflow by rag pipeline - workflow = ( - db.session.query(Workflow) + workflow = db.session.scalar( + select(Workflow) .where( Workflow.tenant_id == pipeline.tenant_id, Workflow.app_id == pipeline.id, Workflow.version == "draft", ) - .first() + .limit(1) ) # return draft workflow @@ -232,28 +228,28 @@ class RagPipelineService: return None # fetch published workflow by workflow_id - workflow = ( - db.session.query(Workflow) + workflow = db.session.scalar( + select(Workflow) .where( Workflow.tenant_id == pipeline.tenant_id, Workflow.app_id == pipeline.id, Workflow.id == pipeline.workflow_id, ) - .first() + .limit(1) ) return workflow def get_published_workflow_by_id(self, pipeline: Pipeline, workflow_id: str) -> Workflow | None: """Fetch a published workflow snapshot by ID for restore operations.""" - workflow = ( - db.session.query(Workflow) + workflow = db.session.scalar( + select(Workflow) .where( Workflow.tenant_id == pipeline.tenant_id, Workflow.app_id == pipeline.id, Workflow.id == workflow_id, ) - .first() + .limit(1) ) if workflow and workflow.version == Workflow.VERSION_DRAFT: raise IsDraftWorkflowError("source workflow must be published") @@ -974,7 +970,7 @@ class RagPipelineService: if invoke_from.value == InvokeFrom.PUBLISHED_PIPELINE: document_id = get_system_segment(variable_pool, SystemVariableKey.DOCUMENT_ID) if document_id: - document = db.session.query(Document).where(Document.id == document_id.value).first() + document = db.session.get(Document, document_id.value) if document: document.indexing_status = IndexingStatus.ERROR document.error = error @@ -1178,15 +1174,15 @@ class RagPipelineService: """ Publish customized pipeline template """ - pipeline = db.session.query(Pipeline).where(Pipeline.id == pipeline_id).first() + pipeline = db.session.get(Pipeline, pipeline_id) if not pipeline: raise ValueError("Pipeline not found") if not pipeline.workflow_id: raise ValueError("Pipeline workflow not found") - workflow = db.session.query(Workflow).where(Workflow.id == pipeline.workflow_id).first() + workflow = db.session.get(Workflow, pipeline.workflow_id) if not workflow: raise ValueError("Workflow not found") - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: dataset = pipeline.retrieve_dataset(session=session) if not dataset: raise ValueError("Dataset not found") @@ -1194,26 +1190,26 @@ class RagPipelineService: # check template name is exist template_name = args.get("name") if template_name: - template = ( - db.session.query(PipelineCustomizedTemplate) + template = db.session.scalar( + select(PipelineCustomizedTemplate) .where( PipelineCustomizedTemplate.name == template_name, PipelineCustomizedTemplate.tenant_id == pipeline.tenant_id, ) - .first() + .limit(1) ) if template: raise ValueError("Template name is already exists") - max_position = ( - db.session.query(func.max(PipelineCustomizedTemplate.position)) - .where(PipelineCustomizedTemplate.tenant_id == pipeline.tenant_id) - .scalar() + max_position = db.session.scalar( + select(func.max(PipelineCustomizedTemplate.position)).where( + PipelineCustomizedTemplate.tenant_id == pipeline.tenant_id + ) ) from services.rag_pipeline.rag_pipeline_dsl_service import RagPipelineDslService - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: rag_pipeline_dsl_service = RagPipelineDslService(session) dsl = rag_pipeline_dsl_service.export_rag_pipeline_dsl(pipeline=pipeline, include_secret=True) if args.get("icon_info") is None: @@ -1239,13 +1235,14 @@ class RagPipelineService: def is_workflow_exist(self, pipeline: Pipeline) -> bool: return ( - db.session.query(Workflow) - .where( - Workflow.tenant_id == pipeline.tenant_id, - Workflow.app_id == pipeline.id, - Workflow.version == Workflow.VERSION_DRAFT, + db.session.scalar( + select(func.count(Workflow.id)).where( + Workflow.tenant_id == pipeline.tenant_id, + Workflow.app_id == pipeline.id, + Workflow.version == Workflow.VERSION_DRAFT, + ) ) - .count() + or 0 ) > 0 def get_node_last_run( @@ -1353,11 +1350,11 @@ class RagPipelineService: def get_recommended_plugins(self, type: str) -> dict: # Query active recommended plugins - query = db.session.query(PipelineRecommendedPlugin).where(PipelineRecommendedPlugin.active == True) + stmt = select(PipelineRecommendedPlugin).where(PipelineRecommendedPlugin.active == True) if type and type != "all": - query = query.where(PipelineRecommendedPlugin.type == type) + stmt = stmt.where(PipelineRecommendedPlugin.type == type) - pipeline_recommended_plugins = query.order_by(PipelineRecommendedPlugin.position.asc()).all() + pipeline_recommended_plugins = db.session.scalars(stmt.order_by(PipelineRecommendedPlugin.position.asc())).all() if not pipeline_recommended_plugins: return { @@ -1396,14 +1393,12 @@ class RagPipelineService: """ Retry error document """ - document_pipeline_execution_log = ( - db.session.query(DocumentPipelineExecutionLog) - .where(DocumentPipelineExecutionLog.document_id == document.id) - .first() + document_pipeline_execution_log = db.session.scalar( + select(DocumentPipelineExecutionLog).where(DocumentPipelineExecutionLog.document_id == document.id).limit(1) ) if not document_pipeline_execution_log: raise ValueError("Document pipeline execution log not found") - pipeline = db.session.query(Pipeline).where(Pipeline.id == document_pipeline_execution_log.pipeline_id).first() + pipeline = db.session.get(Pipeline, document_pipeline_execution_log.pipeline_id) if not pipeline: raise ValueError("Pipeline not found") # convert to app config @@ -1432,23 +1427,23 @@ class RagPipelineService: """ Get datasource plugins """ - dataset: Dataset | None = ( - db.session.query(Dataset) + dataset: Dataset | None = db.session.scalar( + select(Dataset) .where( Dataset.id == dataset_id, Dataset.tenant_id == tenant_id, ) - .first() + .limit(1) ) if not dataset: raise ValueError("Dataset not found") - pipeline: Pipeline | None = ( - db.session.query(Pipeline) + pipeline: Pipeline | None = db.session.scalar( + select(Pipeline) .where( Pipeline.id == dataset.pipeline_id, Pipeline.tenant_id == tenant_id, ) - .first() + .limit(1) ) if not pipeline: raise ValueError("Pipeline not found") @@ -1530,23 +1525,23 @@ class RagPipelineService: """ Get pipeline """ - dataset: Dataset | None = ( - db.session.query(Dataset) + dataset: Dataset | None = db.session.scalar( + select(Dataset) .where( Dataset.id == dataset_id, Dataset.tenant_id == tenant_id, ) - .first() + .limit(1) ) if not dataset: raise ValueError("Dataset not found") - pipeline: Pipeline | None = ( - db.session.query(Pipeline) + pipeline: Pipeline | None = db.session.scalar( + select(Pipeline) .where( Pipeline.id == dataset.pipeline_id, Pipeline.tenant_id == tenant_id, ) - .first() + .limit(1) ) if not pipeline: raise ValueError("Pipeline not found") diff --git a/api/services/retention/conversation/messages_clean_service.py b/api/services/retention/conversation/messages_clean_service.py index 48c3e72af0..0e0dbab2d1 100644 --- a/api/services/retention/conversation/messages_clean_service.py +++ b/api/services/retention/conversation/messages_clean_service.py @@ -3,7 +3,7 @@ import logging import random import time from collections.abc import Sequence -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, TypedDict, cast import sqlalchemy as sa from sqlalchemy import delete, select, tuple_ @@ -158,6 +158,13 @@ class MessagesCleanupMetrics: self._record(self._job_duration_seconds, job_duration_seconds, attributes) +class MessagesCleanStatsDict(TypedDict): + batches: int + total_messages: int + filtered_messages: int + total_deleted: int + + class MessagesCleanService: """ Service for cleaning expired messages based on retention policies. @@ -299,7 +306,7 @@ class MessagesCleanService: task_label=task_label, ) - def run(self) -> dict[str, int]: + def run(self) -> MessagesCleanStatsDict: """ Execute the message cleanup operation. @@ -319,7 +326,7 @@ class MessagesCleanService: job_duration_seconds=time.monotonic() - run_start, ) - def _clean_messages_by_time_range(self) -> dict[str, int]: + def _clean_messages_by_time_range(self) -> MessagesCleanStatsDict: """ Clean messages within a time range using cursor-based pagination. @@ -334,7 +341,7 @@ class MessagesCleanService: Returns: Dict with statistics: batches, filtered_messages, total_deleted """ - stats = { + stats: MessagesCleanStatsDict = { "batches": 0, "total_messages": 0, "filtered_messages": 0, diff --git a/api/services/retention/workflow_run/archive_paid_plan_workflow_run.py b/api/services/retention/workflow_run/archive_paid_plan_workflow_run.py index 2c1f99a3bc..ab60986bfe 100644 --- a/api/services/retention/workflow_run/archive_paid_plan_workflow_run.py +++ b/api/services/retention/workflow_run/archive_paid_plan_workflow_run.py @@ -24,7 +24,7 @@ import zipfile from collections.abc import Sequence from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass, field -from typing import Any +from typing import Any, TypedDict import click from graphon.enums import WorkflowType @@ -49,6 +49,23 @@ from services.retention.workflow_run.constants import ARCHIVE_BUNDLE_NAME, ARCHI logger = logging.getLogger(__name__) +class TableStatsManifestEntry(TypedDict): + row_count: int + checksum: str + size_bytes: int + + +class ArchiveManifestDict(TypedDict): + schema_version: str + workflow_run_id: str + tenant_id: str + app_id: str + workflow_id: str + created_at: str + archived_at: str + tables: dict[str, TableStatsManifestEntry] + + @dataclass class TableStats: """Statistics for a single archived table.""" @@ -472,25 +489,26 @@ class WorkflowRunArchiver: self, run: WorkflowRun, table_stats: list[TableStats], - ) -> dict[str, Any]: + ) -> ArchiveManifestDict: """Generate a manifest for the archived workflow run.""" - return { - "schema_version": ARCHIVE_SCHEMA_VERSION, - "workflow_run_id": run.id, - "tenant_id": run.tenant_id, - "app_id": run.app_id, - "workflow_id": run.workflow_id, - "created_at": run.created_at.isoformat(), - "archived_at": datetime.datetime.now(datetime.UTC).isoformat(), - "tables": { - stat.table_name: { - "row_count": stat.row_count, - "checksum": stat.checksum, - "size_bytes": stat.size_bytes, - } - for stat in table_stats - }, + tables: dict[str, TableStatsManifestEntry] = { + stat.table_name: { + "row_count": stat.row_count, + "checksum": stat.checksum, + "size_bytes": stat.size_bytes, + } + for stat in table_stats } + return ArchiveManifestDict( + schema_version=ARCHIVE_SCHEMA_VERSION, + workflow_run_id=run.id, + tenant_id=run.tenant_id, + app_id=run.app_id, + workflow_id=run.workflow_id, + created_at=run.created_at.isoformat(), + archived_at=datetime.datetime.now(datetime.UTC).isoformat(), + tables=tables, + ) def _build_archive_bundle(self, manifest_data: bytes, table_payloads: dict[str, bytes]) -> bytes: buffer = io.BytesIO() diff --git a/api/services/retention/workflow_run/clear_free_plan_expired_workflow_run_logs.py b/api/services/retention/workflow_run/clear_free_plan_expired_workflow_run_logs.py index 62bc9f5f10..58e8ac57a8 100644 --- a/api/services/retention/workflow_run/clear_free_plan_expired_workflow_run_logs.py +++ b/api/services/retention/workflow_run/clear_free_plan_expired_workflow_run_logs.py @@ -3,7 +3,7 @@ import logging import random import time from collections.abc import Iterable, Sequence -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypedDict import click from sqlalchemy.orm import Session, sessionmaker @@ -12,7 +12,7 @@ from configs import dify_config from enums.cloud_plan import CloudPlan from extensions.ext_database import db from models.workflow import WorkflowRun -from repositories.api_workflow_run_repository import APIWorkflowRunRepository +from repositories.api_workflow_run_repository import APIWorkflowRunRepository, RunsWithRelatedCountsDict from repositories.factory import DifyAPIRepositoryFactory from repositories.sqlalchemy_workflow_trigger_log_repository import SQLAlchemyWorkflowTriggerLogRepository from services.billing_service import BillingService, SubscriptionPlan @@ -24,6 +24,15 @@ if TYPE_CHECKING: from opentelemetry.metrics import Counter, Histogram +class RelatedCountsDict(TypedDict): + node_executions: int + offloads: int + app_logs: int + trigger_logs: int + pauses: int + pause_reasons: int + + class WorkflowRunCleanupMetrics: """ Records low-cardinality OpenTelemetry metrics for workflow run cleanup jobs. @@ -173,6 +182,9 @@ class WorkflowRunCleanupMetrics: self._record(self._job_duration_seconds, job_duration_seconds, attributes) +_RELATED_RECORD_KEYS = ("node_executions", "offloads", "app_logs", "trigger_logs", "pauses", "pause_reasons") + + class WorkflowRunCleanup: def __init__( self, @@ -230,7 +242,7 @@ class WorkflowRunCleanup: total_runs_deleted = 0 total_runs_targeted = 0 - related_totals = self._empty_related_counts() if self.dry_run else None + related_totals: RelatedCountsDict | None = self._empty_related_counts() if self.dry_run else None batch_index = 0 last_seen: tuple[datetime.datetime, str] | None = None status = "success" @@ -312,8 +324,7 @@ class WorkflowRunCleanup: int((time.monotonic() - count_start) * 1000), ) if related_totals is not None: - for key in related_totals: - related_totals[key] += batch_counts.get(key, 0) + self._accumulate_related_counts(related_totals, batch_counts) sample_ids = ", ".join(run.id for run in free_runs[:5]) click.echo( click.style( @@ -332,7 +343,10 @@ class WorkflowRunCleanup: targeted_runs=len(free_runs), skipped_runs=paid_or_skipped, deleted_runs=0, - related_counts={key: batch_counts.get(key, 0) for key in self._empty_related_counts()}, + related_counts={ + k: batch_counts[k] # type: ignore[literal-required] + for k in _RELATED_RECORD_KEYS + }, related_action="would_delete", batch_duration_seconds=time.monotonic() - batch_start, ) @@ -372,7 +386,10 @@ class WorkflowRunCleanup: targeted_runs=len(free_runs), skipped_runs=paid_or_skipped, deleted_runs=counts["runs"], - related_counts={key: counts.get(key, 0) for key in self._empty_related_counts()}, + related_counts={ + k: counts[k] # type: ignore[literal-required] + for k in _RELATED_RECORD_KEYS + }, related_action="deleted", batch_duration_seconds=time.monotonic() - batch_start, ) @@ -506,7 +523,7 @@ class WorkflowRunCleanup: return trigger_repo.count_by_run_ids(run_ids) @staticmethod - def _empty_related_counts() -> dict[str, int]: + def _empty_related_counts() -> RelatedCountsDict: return { "node_executions": 0, "offloads": 0, @@ -517,7 +534,7 @@ class WorkflowRunCleanup: } @staticmethod - def _format_related_counts(counts: dict[str, int]) -> str: + def _format_related_counts(counts: RelatedCountsDict) -> str: return ( f"node_executions {counts['node_executions']}, " f"offloads {counts['offloads']}, " @@ -527,6 +544,15 @@ class WorkflowRunCleanup: f"pause_reasons {counts['pause_reasons']}" ) + @staticmethod + def _accumulate_related_counts(totals: RelatedCountsDict, batch: RunsWithRelatedCountsDict) -> None: + totals["node_executions"] += batch.get("node_executions", 0) + totals["offloads"] += batch.get("offloads", 0) + totals["app_logs"] += batch.get("app_logs", 0) + totals["trigger_logs"] += batch.get("trigger_logs", 0) + totals["pauses"] += batch.get("pauses", 0) + totals["pause_reasons"] += batch.get("pause_reasons", 0) + def _count_node_executions(self, session: Session, runs: Sequence[WorkflowRun]) -> tuple[int, int]: run_ids = [run.id for run in runs] repo = DifyAPIRepositoryFactory.create_api_workflow_node_execution_repository( diff --git a/api/services/retention/workflow_run/delete_archived_workflow_run.py b/api/services/retention/workflow_run/delete_archived_workflow_run.py index 11873bf1b9..937a106710 100644 --- a/api/services/retention/workflow_run/delete_archived_workflow_run.py +++ b/api/services/retention/workflow_run/delete_archived_workflow_run.py @@ -14,7 +14,7 @@ from sqlalchemy.orm import Session, sessionmaker from extensions.ext_database import db from models.workflow import WorkflowRun -from repositories.api_workflow_run_repository import APIWorkflowRunRepository +from repositories.api_workflow_run_repository import APIWorkflowRunRepository, RunsWithRelatedCountsDict from repositories.sqlalchemy_workflow_trigger_log_repository import SQLAlchemyWorkflowTriggerLogRepository @@ -23,7 +23,17 @@ class DeleteResult: run_id: str tenant_id: str success: bool - deleted_counts: dict[str, int] = field(default_factory=dict) + deleted_counts: RunsWithRelatedCountsDict = field( + default_factory=lambda: { # type: ignore[assignment] + "runs": 0, + "node_executions": 0, + "offloads": 0, + "app_logs": 0, + "trigger_logs": 0, + "pauses": 0, + "pause_reasons": 0, + } + ) error: str | None = None elapsed_time: float = 0.0 diff --git a/api/services/summary_index_service.py b/api/services/summary_index_service.py index 12053377e2..8760d60de0 100644 --- a/api/services/summary_index_service.py +++ b/api/services/summary_index_service.py @@ -4,7 +4,7 @@ import logging import time import uuid from datetime import UTC, datetime -from typing import Any +from typing import TypedDict, cast from graphon.model_runtime.entities.llm_entities import LLMUsage from graphon.model_runtime.entities.model_entities import ModelType @@ -25,6 +25,22 @@ from models.enums import SummaryStatus logger = logging.getLogger(__name__) +class SummaryEntryDict(TypedDict): + segment_id: str + segment_position: int + status: str + summary_preview: str | None + error: str | None + created_at: int | None + updated_at: int | None + + +class DocumentSummaryStatusDetailDict(TypedDict): + total_segments: int + summary_status: dict[str, int] + summaries: list[SummaryEntryDict] + + class SummaryIndexService: """Service for generating and managing summary indexes.""" @@ -1352,7 +1368,7 @@ class SummaryIndexService: def get_document_summary_status_detail( document_id: str, dataset_id: str, - ) -> dict[str, Any]: + ) -> DocumentSummaryStatusDetailDict: """ Get detailed summary status for a document. @@ -1403,7 +1419,7 @@ class SummaryIndexService: SummaryStatus.NOT_STARTED: 0, } - summary_list = [] + summary_list: list[SummaryEntryDict] = [] for segment in segments: summary = summary_map.get(segment.id) if summary: @@ -1438,8 +1454,8 @@ class SummaryIndexService: } ) - return { - "total_segments": total_segments, - "summary_status": status_counts, - "summaries": summary_list, - } + return DocumentSummaryStatusDetailDict( + total_segments=total_segments, + summary_status=cast(dict[str, int], status_counts), + summaries=summary_list, + ) diff --git a/api/services/tag_service.py b/api/services/tag_service.py index 194622bd86..1882c855ea 100644 --- a/api/services/tag_service.py +++ b/api/services/tag_service.py @@ -2,6 +2,7 @@ import uuid import sqlalchemy as sa from flask_login import current_user +from pydantic import BaseModel, Field from sqlalchemy import func, select from werkzeug.exceptions import NotFound @@ -11,6 +12,28 @@ from models.enums import TagType from models.model import App, Tag, TagBinding +class SaveTagPayload(BaseModel): + name: str = Field(min_length=1, max_length=50) + type: TagType + + +class UpdateTagPayload(BaseModel): + name: str = Field(min_length=1, max_length=50) + type: TagType + + +class TagBindingCreatePayload(BaseModel): + tag_ids: list[str] + target_id: str + type: TagType + + +class TagBindingDeletePayload(BaseModel): + tag_id: str + target_id: str + type: TagType + + class TagService: @staticmethod def get_tags(tag_type: str, current_tenant_id: str, keyword: str | None = None): @@ -78,12 +101,12 @@ class TagService: return tags or [] @staticmethod - def save_tags(args: dict) -> Tag: - if TagService.get_tag_by_tag_name(args["type"], current_user.current_tenant_id, args["name"]): + def save_tags(payload: SaveTagPayload) -> Tag: + if TagService.get_tag_by_tag_name(payload.type, current_user.current_tenant_id, payload.name): raise ValueError("Tag name already exists") tag = Tag( - name=args["name"], - type=TagType(args["type"]), + name=payload.name, + type=TagType(payload.type), created_by=current_user.id, tenant_id=current_user.current_tenant_id, ) @@ -93,13 +116,24 @@ class TagService: return tag @staticmethod - def update_tags(args: dict, tag_id: str) -> Tag: - if TagService.get_tag_by_tag_name(args.get("type", ""), current_user.current_tenant_id, args.get("name", "")): - raise ValueError("Tag name already exists") + def update_tags(payload: UpdateTagPayload, tag_id: str) -> Tag: tag = db.session.scalar(select(Tag).where(Tag.id == tag_id).limit(1)) if not tag: raise NotFound("Tag not found") - tag.name = args["name"] + if payload.name != tag.name: + existing = db.session.scalar( + select(Tag) + .where( + Tag.name == payload.name, + Tag.tenant_id == current_user.current_tenant_id, + Tag.type == tag.type, + Tag.id != tag_id, + ) + .limit(1) + ) + if existing: + raise ValueError("Tag name already exists") + tag.name = payload.name db.session.commit() return tag @@ -122,21 +156,19 @@ class TagService: db.session.commit() @staticmethod - def save_tag_binding(args): - # check if target exists - TagService.check_target_exists(args["type"], args["target_id"]) - # save tag binding - for tag_id in args["tag_ids"]: + def save_tag_binding(payload: TagBindingCreatePayload): + TagService.check_target_exists(payload.type, payload.target_id) + for tag_id in payload.tag_ids: tag_binding = db.session.scalar( select(TagBinding) - .where(TagBinding.tag_id == tag_id, TagBinding.target_id == args["target_id"]) + .where(TagBinding.tag_id == tag_id, TagBinding.target_id == payload.target_id) .limit(1) ) if tag_binding: continue new_tag_binding = TagBinding( tag_id=tag_id, - target_id=args["target_id"], + target_id=payload.target_id, tenant_id=current_user.current_tenant_id, created_by=current_user.id, ) @@ -144,17 +176,15 @@ class TagService: db.session.commit() @staticmethod - def delete_tag_binding(args): - # check if target exists - TagService.check_target_exists(args["type"], args["target_id"]) - # delete tag binding - tag_bindings = db.session.scalar( + def delete_tag_binding(payload: TagBindingDeletePayload): + TagService.check_target_exists(payload.type, payload.target_id) + tag_binding = db.session.scalar( select(TagBinding) - .where(TagBinding.target_id == args["target_id"], TagBinding.tag_id == args["tag_id"]) + .where(TagBinding.target_id == payload.target_id, TagBinding.tag_id == payload.tag_id) .limit(1) ) - if tag_bindings: - db.session.delete(tag_bindings) + if tag_binding: + db.session.delete(tag_binding) db.session.commit() @staticmethod diff --git a/api/services/tools/mcp_tools_manage_service.py b/api/services/tools/mcp_tools_manage_service.py index deb26438a8..690b06ea7d 100644 --- a/api/services/tools/mcp_tools_manage_service.py +++ b/api/services/tools/mcp_tools_manage_service.py @@ -285,7 +285,7 @@ class MCPToolManageService: # Batch query all users to avoid N+1 problem user_ids = {provider.user_id for provider in mcp_providers} - users = self._session.query(Account).where(Account.id.in_(user_ids)).all() + users = self._session.scalars(select(Account).where(Account.id.in_(user_ids))).all() user_name_map = {user.id: user.name for user in users} return [ diff --git a/api/services/tools/tools_transform_service.py b/api/services/tools/tools_transform_service.py index 7cd61e3162..b24f001133 100644 --- a/api/services/tools/tools_transform_service.py +++ b/api/services/tools/tools_transform_service.py @@ -1,4 +1,3 @@ -import json import logging from collections.abc import Mapping from typing import Any, Union @@ -21,6 +20,7 @@ from core.tools.entities.tool_entities import ( ApiProviderAuthType, ToolParameter, ToolProviderType, + emoji_icon_adapter, ) from core.tools.plugin_tool.provider import PluginToolProviderController from core.tools.utils.encryption import create_provider_encrypter, create_tool_provider_encrypter @@ -53,11 +53,14 @@ class ToolTransformService: elif provider_type in {ToolProviderType.API, ToolProviderType.WORKFLOW}: try: if isinstance(icon, str): - return json.loads(icon) - return icon - except (json.JSONDecodeError, ValueError): + parsed = emoji_icon_adapter.validate_json(icon) + return {"background": parsed["background"], "content": parsed["content"]} + return {"background": icon["background"], "content": icon["content"]} + except (ValueError, ValidationError, KeyError): return {"background": "#252525", "content": "\ud83d\ude01"} elif provider_type == ToolProviderType.MCP: + if isinstance(icon, Mapping): + return {"background": icon.get("background", ""), "content": icon.get("content", "")} return icon return "" diff --git a/api/services/tools/workflow_tools_manage_service.py b/api/services/tools/workflow_tools_manage_service.py index fb6b5bea24..8f5144c866 100644 --- a/api/services/tools/workflow_tools_manage_service.py +++ b/api/services/tools/workflow_tools_manage_service.py @@ -3,12 +3,12 @@ import logging from datetime import datetime from graphon.model_runtime.utils.encoders import jsonable_encoder -from sqlalchemy import or_, select +from sqlalchemy import delete, or_, select from sqlalchemy.orm import Session from core.tools.__base.tool_provider import ToolProviderController from core.tools.entities.api_entities import ToolApiEntity, ToolProviderApiEntity -from core.tools.entities.tool_entities import WorkflowToolParameterConfiguration +from core.tools.entities.tool_entities import WorkflowToolParameterConfiguration, emoji_icon_adapter from core.tools.tool_label_manager import ToolLabelManager from core.tools.utils.workflow_configuration_sync import WorkflowToolConfigurationUtils from core.tools.workflow_as_tool.provider import WorkflowToolProviderController @@ -42,20 +42,22 @@ class WorkflowToolManageService: labels: list[str] | None = None, ): # check if the name is unique - existing_workflow_tool_provider = ( - db.session.query(WorkflowToolProvider) + existing_workflow_tool_provider = db.session.scalar( + select(WorkflowToolProvider) .where( WorkflowToolProvider.tenant_id == tenant_id, # name or app_id or_(WorkflowToolProvider.name == name, WorkflowToolProvider.app_id == workflow_app_id), ) - .first() + .limit(1) ) if existing_workflow_tool_provider is not None: raise ValueError(f"Tool with name {name} or app_id {workflow_app_id} already exists") - app: App | None = db.session.query(App).where(App.id == workflow_app_id, App.tenant_id == tenant_id).first() + app: App | None = db.session.scalar( + select(App).where(App.id == workflow_app_id, App.tenant_id == tenant_id).limit(1) + ) if app is None: raise ValueError(f"App {workflow_app_id} not found") @@ -122,30 +124,30 @@ class WorkflowToolManageService: :return: the updated tool """ # check if the name is unique - existing_workflow_tool_provider = ( - db.session.query(WorkflowToolProvider) + existing_workflow_tool_provider = db.session.scalar( + select(WorkflowToolProvider) .where( WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.name == name, WorkflowToolProvider.id != workflow_tool_id, ) - .first() + .limit(1) ) if existing_workflow_tool_provider is not None: raise ValueError(f"Tool with name {name} already exists") - workflow_tool_provider: WorkflowToolProvider | None = ( - db.session.query(WorkflowToolProvider) + workflow_tool_provider: WorkflowToolProvider | None = db.session.scalar( + select(WorkflowToolProvider) .where(WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.id == workflow_tool_id) - .first() + .limit(1) ) if workflow_tool_provider is None: raise ValueError(f"Tool {workflow_tool_id} not found") - app: App | None = ( - db.session.query(App).where(App.id == workflow_tool_provider.app_id, App.tenant_id == tenant_id).first() + app: App | None = db.session.scalar( + select(App).where(App.id == workflow_tool_provider.app_id, App.tenant_id == tenant_id).limit(1) ) if app is None: @@ -234,9 +236,11 @@ class WorkflowToolManageService: :param tenant_id: the tenant id :param workflow_tool_id: the workflow tool id """ - db.session.query(WorkflowToolProvider).where( - WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.id == workflow_tool_id - ).delete() + db.session.execute( + delete(WorkflowToolProvider).where( + WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.id == workflow_tool_id + ) + ) db.session.commit() @@ -251,10 +255,10 @@ class WorkflowToolManageService: :param workflow_tool_id: the workflow tool id :return: the tool """ - db_tool: WorkflowToolProvider | None = ( - db.session.query(WorkflowToolProvider) + db_tool: WorkflowToolProvider | None = db.session.scalar( + select(WorkflowToolProvider) .where(WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.id == workflow_tool_id) - .first() + .limit(1) ) return cls._get_workflow_tool(tenant_id, db_tool) @@ -267,10 +271,10 @@ class WorkflowToolManageService: :param workflow_app_id: the workflow app id :return: the tool """ - db_tool: WorkflowToolProvider | None = ( - db.session.query(WorkflowToolProvider) + db_tool: WorkflowToolProvider | None = db.session.scalar( + select(WorkflowToolProvider) .where(WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.app_id == workflow_app_id) - .first() + .limit(1) ) return cls._get_workflow_tool(tenant_id, db_tool) @@ -284,8 +288,8 @@ class WorkflowToolManageService: if db_tool is None: raise ValueError("Tool not found") - workflow_app: App | None = ( - db.session.query(App).where(App.id == db_tool.app_id, App.tenant_id == db_tool.tenant_id).first() + workflow_app: App | None = db.session.scalar( + select(App).where(App.id == db_tool.app_id, App.tenant_id == db_tool.tenant_id).limit(1) ) if workflow_app is None: @@ -309,7 +313,7 @@ class WorkflowToolManageService: "label": db_tool.label, "workflow_tool_id": db_tool.id, "workflow_app_id": db_tool.app_id, - "icon": json.loads(db_tool.icon), + "icon": emoji_icon_adapter.validate_json(db_tool.icon), "description": db_tool.description, "parameters": jsonable_encoder(db_tool.parameter_configurations), "output_schema": output_schema, @@ -331,10 +335,10 @@ class WorkflowToolManageService: :param workflow_tool_id: the workflow tool id :return: the list of tools """ - db_tool: WorkflowToolProvider | None = ( - db.session.query(WorkflowToolProvider) + db_tool: WorkflowToolProvider | None = db.session.scalar( + select(WorkflowToolProvider) .where(WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.id == workflow_tool_id) - .first() + .limit(1) ) if db_tool is None: diff --git a/api/services/trigger/webhook_service.py b/api/services/trigger/webhook_service.py index 844dddfb65..c624a22e41 100644 --- a/api/services/trigger/webhook_service.py +++ b/api/services/trigger/webhook_service.py @@ -3,7 +3,7 @@ import logging import mimetypes import secrets from collections.abc import Callable, Mapping, Sequence -from typing import Any +from typing import Any, NotRequired, TypedDict import orjson from flask import request @@ -51,6 +51,26 @@ logger = logging.getLogger(__name__) _file_access_controller = DatabaseFileAccessController() +class RawWebhookDataDict(TypedDict): + method: str + headers: dict[str, str] + query_params: dict[str, str] + body: dict[str, Any] + files: dict[str, Any] + + +class ValidationResultDict(TypedDict): + valid: bool + error: NotRequired[str] + + +class WorkflowInputsDict(TypedDict): + webhook_data: RawWebhookDataDict + webhook_headers: dict[str, str] + webhook_query_params: dict[str, str] + webhook_body: dict[str, Any] + + class WebhookService: """Service for handling webhook operations.""" @@ -146,7 +166,7 @@ class WebhookService: @classmethod def extract_and_validate_webhook_data( cls, webhook_trigger: WorkflowWebhookTrigger, node_config: NodeConfigDict - ) -> dict[str, Any]: + ) -> RawWebhookDataDict: """Extract and validate webhook data in a single unified process. Args: @@ -166,7 +186,7 @@ class WebhookService: node_data = WebhookData.model_validate(node_config["data"], from_attributes=True) validation_result = cls._validate_http_metadata(raw_data, node_data) if not validation_result["valid"]: - raise ValueError(validation_result["error"]) + raise ValueError(validation_result.get("error", "Validation failed")) # Process and validate data according to configuration processed_data = cls._process_and_validate_data(raw_data, node_data) @@ -174,7 +194,7 @@ class WebhookService: return processed_data @classmethod - def extract_webhook_data(cls, webhook_trigger: WorkflowWebhookTrigger) -> dict[str, Any]: + def extract_webhook_data(cls, webhook_trigger: WorkflowWebhookTrigger) -> RawWebhookDataDict: """Extract raw data from incoming webhook request without type conversion. Args: @@ -190,7 +210,7 @@ class WebhookService: """ cls._validate_content_length() - data = { + data: RawWebhookDataDict = { "method": request.method, "headers": dict(request.headers), "query_params": dict(request.args), @@ -224,7 +244,7 @@ class WebhookService: return data @classmethod - def _process_and_validate_data(cls, raw_data: dict[str, Any], node_data: WebhookData) -> dict[str, Any]: + def _process_and_validate_data(cls, raw_data: RawWebhookDataDict, node_data: WebhookData) -> RawWebhookDataDict: """Process and validate webhook data according to node configuration. Args: @@ -665,7 +685,7 @@ class WebhookService: raise ValueError(f"Required header missing: {header_name}") @classmethod - def _validate_http_metadata(cls, webhook_data: dict[str, Any], node_data: WebhookData) -> dict[str, Any]: + def _validate_http_metadata(cls, webhook_data: RawWebhookDataDict, node_data: WebhookData) -> ValidationResultDict: """Validate HTTP method and content-type. Args: @@ -709,7 +729,7 @@ class WebhookService: return content_type.split(";")[0].strip() @classmethod - def _validation_error(cls, error_message: str) -> dict[str, Any]: + def _validation_error(cls, error_message: str) -> ValidationResultDict: """Create a standard validation error response. Args: @@ -730,7 +750,7 @@ class WebhookService: return False @classmethod - def build_workflow_inputs(cls, webhook_data: dict[str, Any]) -> dict[str, Any]: + def build_workflow_inputs(cls, webhook_data: RawWebhookDataDict) -> WorkflowInputsDict: """Construct workflow inputs payload from webhook data. Args: @@ -748,7 +768,7 @@ class WebhookService: @classmethod def trigger_workflow_execution( - cls, webhook_trigger: WorkflowWebhookTrigger, webhook_data: dict[str, Any], workflow: Workflow + cls, webhook_trigger: WorkflowWebhookTrigger, webhook_data: RawWebhookDataDict, workflow: Workflow ) -> None: """Trigger workflow execution via AsyncWorkflowService. diff --git a/api/services/vector_service.py b/api/services/vector_service.py index e7266cb8e9..9827c8dfbc 100644 --- a/api/services/vector_service.py +++ b/api/services/vector_service.py @@ -6,6 +6,7 @@ from sqlalchemy import delete, select from core.model_manager import ModelInstance, ModelManager from core.rag.datasource.keyword.keyword_factory import Keyword from core.rag.datasource.vdb.vector_factory import Vector +from core.rag.entities import ParentMode from core.rag.index_processor.constant.doc_type import DocType from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType from core.rag.index_processor.index_processor_base import BaseIndexProcessor @@ -15,7 +16,6 @@ from extensions.ext_database import db from models import UploadFile from models.dataset import ChildChunk, Dataset, DatasetProcessRule, DocumentSegment, SegmentAttachmentBinding from models.dataset import Document as DatasetDocument -from services.entities.knowledge_entities.knowledge_entities import ParentMode logger = logging.getLogger(__name__) diff --git a/api/services/website_service.py b/api/services/website_service.py index 6a521a9cc0..2471c2cee8 100644 --- a/api/services/website_service.py +++ b/api/services/website_service.py @@ -3,7 +3,7 @@ from __future__ import annotations import datetime import json from dataclasses import dataclass -from typing import Any +from typing import Any, NotRequired, TypedDict, cast import httpx from flask_login import current_user @@ -126,6 +126,15 @@ class WebsiteCrawlStatusApiRequest: return cls(provider=provider, job_id=job_id) +class CrawlStatusDict(TypedDict): + status: str + job_id: str + total: int + current: int + data: list[Any] + time_consuming: NotRequired[str | float] + + class WebsiteService: """Service class for website crawling operations using different providers.""" @@ -261,13 +270,13 @@ class WebsiteService: return {"status": "active", "job_id": response.json().get("data", {}).get("taskId")} @classmethod - def get_crawl_status(cls, job_id: str, provider: str) -> dict[str, Any]: + def get_crawl_status(cls, job_id: str, provider: str) -> CrawlStatusDict: """Get crawl status using string parameters.""" api_request = WebsiteCrawlStatusApiRequest(provider=provider, job_id=job_id) return cls.get_crawl_status_typed(api_request) @classmethod - def get_crawl_status_typed(cls, api_request: WebsiteCrawlStatusApiRequest) -> dict[str, Any]: + def get_crawl_status_typed(cls, api_request: WebsiteCrawlStatusApiRequest) -> CrawlStatusDict: """Get crawl status using typed request.""" api_key, config = cls._get_credentials_and_config(current_user.current_tenant_id, api_request.provider) @@ -281,10 +290,10 @@ class WebsiteService: raise ValueError("Invalid provider") @classmethod - def _get_firecrawl_status(cls, job_id: str, api_key: str, config: dict) -> dict[str, Any]: + def _get_firecrawl_status(cls, job_id: str, api_key: str, config: dict) -> CrawlStatusDict: firecrawl_app = FirecrawlApp(api_key=api_key, base_url=config.get("base_url")) result: CrawlStatusResponse = firecrawl_app.check_crawl_status(job_id) - crawl_status_data: dict[str, Any] = { + crawl_status_data: CrawlStatusDict = { "status": result["status"], "job_id": job_id, "total": result["total"] or 0, @@ -302,18 +311,18 @@ class WebsiteService: return crawl_status_data @classmethod - def _get_watercrawl_status(cls, job_id: str, api_key: str, config: dict[str, Any]) -> dict[str, Any]: - return dict(WaterCrawlProvider(api_key, config.get("base_url")).get_crawl_status(job_id)) + def _get_watercrawl_status(cls, job_id: str, api_key: str, config: dict[str, Any]) -> CrawlStatusDict: + return cast(CrawlStatusDict, dict(WaterCrawlProvider(api_key, config.get("base_url")).get_crawl_status(job_id))) @classmethod - def _get_jinareader_status(cls, job_id: str, api_key: str) -> dict[str, Any]: + def _get_jinareader_status(cls, job_id: str, api_key: str) -> CrawlStatusDict: response = _adaptive_http_client.post( "https://adaptivecrawlstatus-kir3wx7b3a-uc.a.run.app", headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}, json={"taskId": job_id}, ) data = response.json().get("data", {}) - crawl_status_data = { + crawl_status_data: CrawlStatusDict = { "status": data.get("status", "active"), "job_id": job_id, "total": len(data.get("urls", [])), diff --git a/api/services/workflow/workflow_converter.py b/api/services/workflow/workflow_converter.py index c1ad3f33ad..1582bcd46c 100644 --- a/api/services/workflow/workflow_converter.py +++ b/api/services/workflow/workflow_converter.py @@ -170,34 +170,38 @@ class WorkflowConverter: graph = self._append_node(graph, llm_node) - if new_app_mode == AppMode.WORKFLOW: - # convert to end node by app mode - end_node = self._convert_to_end_node() - graph = self._append_node(graph, end_node) - else: - answer_node = self._convert_to_answer_node() - graph = self._append_node(graph, answer_node) - app_model_config_dict = app_config.app_model_config_dict - # features - if new_app_mode == AppMode.ADVANCED_CHAT: - features = { - "opening_statement": app_model_config_dict.get("opening_statement"), - "suggested_questions": app_model_config_dict.get("suggested_questions"), - "suggested_questions_after_answer": app_model_config_dict.get("suggested_questions_after_answer"), - "speech_to_text": app_model_config_dict.get("speech_to_text"), - "text_to_speech": app_model_config_dict.get("text_to_speech"), - "file_upload": app_model_config_dict.get("file_upload"), - "sensitive_word_avoidance": app_model_config_dict.get("sensitive_word_avoidance"), - "retriever_resource": app_model_config_dict.get("retriever_resource"), - } - else: - features = { - "text_to_speech": app_model_config_dict.get("text_to_speech"), - "file_upload": app_model_config_dict.get("file_upload"), - "sensitive_word_avoidance": app_model_config_dict.get("sensitive_word_avoidance"), - } + match new_app_mode: + case AppMode.WORKFLOW: + end_node = self._convert_to_end_node() + graph = self._append_node(graph, end_node) + features = { + "text_to_speech": app_model_config_dict.get("text_to_speech"), + "file_upload": app_model_config_dict.get("file_upload"), + "sensitive_word_avoidance": app_model_config_dict.get("sensitive_word_avoidance"), + } + case AppMode.ADVANCED_CHAT: + answer_node = self._convert_to_answer_node() + graph = self._append_node(graph, answer_node) + features = { + "opening_statement": app_model_config_dict.get("opening_statement"), + "suggested_questions": app_model_config_dict.get("suggested_questions"), + "suggested_questions_after_answer": app_model_config_dict.get("suggested_questions_after_answer"), + "speech_to_text": app_model_config_dict.get("speech_to_text"), + "text_to_speech": app_model_config_dict.get("text_to_speech"), + "file_upload": app_model_config_dict.get("file_upload"), + "sensitive_word_avoidance": app_model_config_dict.get("sensitive_word_avoidance"), + "retriever_resource": app_model_config_dict.get("retriever_resource"), + } + case _: + answer_node = self._convert_to_answer_node() + graph = self._append_node(graph, answer_node) + features = { + "text_to_speech": app_model_config_dict.get("text_to_speech"), + "file_upload": app_model_config_dict.get("file_upload"), + "sensitive_word_avoidance": app_model_config_dict.get("sensitive_word_avoidance"), + } # create workflow record workflow = Workflow( @@ -220,19 +224,23 @@ class WorkflowConverter: def _convert_to_app_config(self, app_model: App, app_model_config: AppModelConfig) -> EasyUIBasedAppConfig: app_mode_enum = AppMode.value_of(app_model.mode) app_config: EasyUIBasedAppConfig - if app_mode_enum == AppMode.AGENT_CHAT or app_model.is_agent: - app_model.mode = AppMode.AGENT_CHAT - app_config = AgentChatAppConfigManager.get_app_config( - app_model=app_model, app_model_config=app_model_config - ) - elif app_mode_enum == AppMode.CHAT: - app_config = ChatAppConfigManager.get_app_config(app_model=app_model, app_model_config=app_model_config) - elif app_mode_enum == AppMode.COMPLETION: - app_config = CompletionAppConfigManager.get_app_config( - app_model=app_model, app_model_config=app_model_config - ) - else: - raise ValueError("Invalid app mode") + effective_mode = ( + AppMode.AGENT_CHAT if app_model.is_agent and app_mode_enum != AppMode.AGENT_CHAT else app_mode_enum + ) + match effective_mode: + case AppMode.AGENT_CHAT: + app_model.mode = AppMode.AGENT_CHAT + app_config = AgentChatAppConfigManager.get_app_config( + app_model=app_model, app_model_config=app_model_config + ) + case AppMode.CHAT: + app_config = ChatAppConfigManager.get_app_config(app_model=app_model, app_model_config=app_model_config) + case AppMode.COMPLETION: + app_config = CompletionAppConfigManager.get_app_config( + app_model=app_model, app_model_config=app_model_config + ) + case _: + raise ValueError("Invalid app mode") return app_config diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index 8f365c7c51..eaffb60c63 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -38,6 +38,7 @@ from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfig from core.app.apps.workflow.app_config_manager import WorkflowAppConfigManager from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom, build_dify_run_context from core.app.file_access import DatabaseFileAccessController +from core.entities import PluginCredentialType from core.plugin.impl.model_runtime_factory import create_plugin_model_assembly, create_plugin_provider_manager from core.repositories import DifyCoreRepositoryFactory from core.repositories.human_input_repository import FormCreateParams, HumanInputFormRepositoryImpl @@ -66,7 +67,6 @@ from models.tools import WorkflowToolProvider from models.workflow import Workflow, WorkflowNodeExecutionModel, WorkflowNodeExecutionTriggeredFrom, WorkflowType from repositories.factory import DifyAPIRepositoryFactory from services.billing_service import BillingService -from services.enterprise.plugin_manager_service import PluginCredentialType from services.errors.app import ( IsDraftWorkflowError, TriggerNodeLimitExceededError, @@ -635,7 +635,7 @@ class WorkflowService: # If we can't determine the status, assume load balancing is not enabled return False - def _get_load_balancing_configs(self, tenant_id: str, provider: str, model_name: str) -> list[dict]: + def _get_load_balancing_configs(self, tenant_id: str, provider: str, model_name: str) -> list[dict[str, Any]]: """ Get all load balancing configurations for a model. @@ -659,7 +659,7 @@ class WorkflowService: _, custom_configs = model_load_balancing_service.get_load_balancing_configs( tenant_id=tenant_id, provider=provider, model=model_name, model_type="llm", config_from="custom-model" ) - all_configs = configs + custom_configs + all_configs = cast(list[dict[str, Any]], configs) + cast(list[dict[str, Any]], custom_configs) return [config for config in all_configs if config.get("credential_id")] @@ -834,7 +834,7 @@ class WorkflowService: if workflow_node_execution is None: raise ValueError(f"WorkflowNodeExecution with id {node_execution.id} not found after saving") - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: outputs = workflow_node_execution.load_full_outputs(session, storage) with Session(bind=db.engine) as session, session.begin(): @@ -1417,16 +1417,17 @@ class WorkflowService: self._validate_human_input_node_data(node_data) def validate_features_structure(self, app_model: App, features: dict): - if app_model.mode == AppMode.ADVANCED_CHAT: - return AdvancedChatAppConfigManager.config_validate( - tenant_id=app_model.tenant_id, config=features, only_structure_validate=True - ) - elif app_model.mode == AppMode.WORKFLOW: - return WorkflowAppConfigManager.config_validate( - tenant_id=app_model.tenant_id, config=features, only_structure_validate=True - ) - else: - raise ValueError(f"Invalid app mode: {app_model.mode}") + match app_model.mode: + case AppMode.ADVANCED_CHAT: + return AdvancedChatAppConfigManager.config_validate( + tenant_id=app_model.tenant_id, config=features, only_structure_validate=True + ) + case AppMode.WORKFLOW: + return WorkflowAppConfigManager.config_validate( + tenant_id=app_model.tenant_id, config=features, only_structure_validate=True + ) + case _: + raise ValueError(f"Invalid app mode: {app_model.mode}") def _validate_human_input_node_data(self, node_data: dict) -> None: """ diff --git a/api/tasks/annotation/batch_import_annotations_task.py b/api/tasks/annotation/batch_import_annotations_task.py index c734e1321b..89844ef44b 100644 --- a/api/tasks/annotation/batch_import_annotations_task.py +++ b/api/tasks/annotation/batch_import_annotations_task.py @@ -3,6 +3,7 @@ import time import click from celery import shared_task +from sqlalchemy import select from werkzeug.exceptions import NotFound from core.db.session_factory import session_factory @@ -35,7 +36,9 @@ def batch_import_annotations_task(job_id: str, content_list: list[dict], app_id: with session_factory.create_session() as session: # get app info - app = session.query(App).where(App.id == app_id, App.tenant_id == tenant_id, App.status == "normal").first() + app = session.scalar( + select(App).where(App.id == app_id, App.tenant_id == tenant_id, App.status == "normal").limit(1) + ) if app: try: @@ -53,8 +56,8 @@ def batch_import_annotations_task(job_id: str, content_list: list[dict], app_id: ) documents.append(document) # if annotation reply is enabled , batch add annotations' index - app_annotation_setting = ( - session.query(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).first() + app_annotation_setting = session.scalar( + select(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).limit(1) ) if app_annotation_setting: diff --git a/api/tasks/annotation/disable_annotation_reply_task.py b/api/tasks/annotation/disable_annotation_reply_task.py index 41cf7ccbf6..6a9b52e7e5 100644 --- a/api/tasks/annotation/disable_annotation_reply_task.py +++ b/api/tasks/annotation/disable_annotation_reply_task.py @@ -24,14 +24,16 @@ def disable_annotation_reply_task(job_id: str, app_id: str, tenant_id: str): start_at = time.perf_counter() # get app info with session_factory.create_session() as session: - app = session.query(App).where(App.id == app_id, App.tenant_id == tenant_id, App.status == "normal").first() + app = session.scalar( + select(App).where(App.id == app_id, App.tenant_id == tenant_id, App.status == "normal").limit(1) + ) annotations_exists = session.scalar(select(exists().where(MessageAnnotation.app_id == app_id))) if not app: logger.info(click.style(f"App not found: {app_id}", fg="red")) return - app_annotation_setting = ( - session.query(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).first() + app_annotation_setting = session.scalar( + select(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).limit(1) ) if not app_annotation_setting: diff --git a/api/tasks/annotation/enable_annotation_reply_task.py b/api/tasks/annotation/enable_annotation_reply_task.py index 2c07fe0f31..4cbca13a92 100644 --- a/api/tasks/annotation/enable_annotation_reply_task.py +++ b/api/tasks/annotation/enable_annotation_reply_task.py @@ -36,7 +36,9 @@ def enable_annotation_reply_task( start_at = time.perf_counter() # get app info with session_factory.create_session() as session: - app = session.query(App).where(App.id == app_id, App.tenant_id == tenant_id, App.status == "normal").first() + app = session.scalar( + select(App).where(App.id == app_id, App.tenant_id == tenant_id, App.status == "normal").limit(1) + ) if not app: logger.info(click.style(f"App not found: {app_id}", fg="red")) @@ -51,8 +53,8 @@ def enable_annotation_reply_task( dataset_collection_binding = DatasetCollectionBindingService.get_dataset_collection_binding( embedding_provider_name, embedding_model_name, CollectionBindingType.ANNOTATION ) - annotation_setting = ( - session.query(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).first() + annotation_setting = session.scalar( + select(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).limit(1) ) if annotation_setting: if dataset_collection_binding.id != annotation_setting.collection_binding_id: diff --git a/api/tasks/batch_clean_document_task.py b/api/tasks/batch_clean_document_task.py index 747106d373..66aafc30b9 100644 --- a/api/tasks/batch_clean_document_task.py +++ b/api/tasks/batch_clean_document_task.py @@ -73,7 +73,7 @@ def batch_clean_document_task(document_ids: list[str], dataset_id: str, doc_form try: # Fetch dataset in a fresh session to avoid DetachedInstanceError with session_factory.create_session() as session: - dataset = session.query(Dataset).where(Dataset.id == dataset_id).first() + dataset = session.scalar(select(Dataset).where(Dataset.id == dataset_id).limit(1)) if not dataset: logger.warning("Dataset not found for vector index cleanup, dataset_id: %s", dataset_id) else: @@ -92,7 +92,7 @@ def batch_clean_document_task(document_ids: list[str], dataset_id: str, doc_form # ============ Step 3: Delete metadata binding (separate short transaction) ============ try: with session_factory.create_session() as session: - deleted_count = ( + deleted_count = int( session.query(DatasetMetadataBinding) .where( DatasetMetadataBinding.dataset_id == dataset_id, diff --git a/api/tasks/batch_create_segment_to_index_task.py b/api/tasks/batch_create_segment_to_index_task.py index 20335d9b9f..77feea47a2 100644 --- a/api/tasks/batch_create_segment_to_index_task.py +++ b/api/tasks/batch_create_segment_to_index_task.py @@ -8,7 +8,7 @@ import click import pandas as pd from celery import shared_task from graphon.model_runtime.entities.model_entities import ModelType -from sqlalchemy import func +from sqlalchemy import func, select from core.db.session_factory import session_factory from core.model_manager import ModelManager @@ -140,10 +140,8 @@ def batch_create_segment_to_index_task( content = segment["content"] doc_id = str(uuid.uuid4()) segment_hash = helper.generate_text_hash(content) - max_position = ( - session.query(func.max(DocumentSegment.position)) - .where(DocumentSegment.document_id == document_config["id"]) - .scalar() + max_position = session.scalar( + select(func.max(DocumentSegment.position)).where(DocumentSegment.document_id == document_config["id"]) ) segment_document = DocumentSegment( tenant_id=tenant_id, diff --git a/api/tasks/clean_notion_document_task.py b/api/tasks/clean_notion_document_task.py index c22ee761d8..e3be24ac74 100644 --- a/api/tasks/clean_notion_document_task.py +++ b/api/tasks/clean_notion_document_task.py @@ -26,7 +26,7 @@ def clean_notion_document_task(document_ids: list[str], dataset_id: str): total_index_node_ids = [] with session_factory.create_session() as session: - dataset = session.query(Dataset).where(Dataset.id == dataset_id).first() + dataset = session.scalar(select(Dataset).where(Dataset.id == dataset_id).limit(1)) if not dataset: raise Exception("Document has no dataset") @@ -41,7 +41,7 @@ def clean_notion_document_task(document_ids: list[str], dataset_id: str): total_index_node_ids.extend([segment.index_node_id for segment in segments]) with session_factory.create_session() as session: - dataset = session.query(Dataset).where(Dataset.id == dataset_id).first() + dataset = session.scalar(select(Dataset).where(Dataset.id == dataset_id).limit(1)) if dataset: index_processor.clean( dataset, total_index_node_ids, with_keywords=True, delete_child_chunks=True, delete_summaries=True diff --git a/api/tasks/create_segment_to_index_task.py b/api/tasks/create_segment_to_index_task.py index b3cbc73d6e..3448325104 100644 --- a/api/tasks/create_segment_to_index_task.py +++ b/api/tasks/create_segment_to_index_task.py @@ -3,6 +3,7 @@ import time import click from celery import shared_task +from sqlalchemy import select, update from core.db.session_factory import session_factory from core.rag.index_processor.index_processor_factory import IndexProcessorFactory @@ -27,7 +28,7 @@ def create_segment_to_index_task(segment_id: str, keywords: list[str] | None = N start_at = time.perf_counter() with session_factory.create_session() as session: - segment = session.query(DocumentSegment).where(DocumentSegment.id == segment_id).first() + segment = session.scalar(select(DocumentSegment).where(DocumentSegment.id == segment_id).limit(1)) if not segment: logger.info(click.style(f"Segment not found: {segment_id}", fg="red")) return @@ -39,11 +40,10 @@ def create_segment_to_index_task(segment_id: str, keywords: list[str] | None = N try: # update segment status to indexing - session.query(DocumentSegment).filter_by(id=segment.id).update( - { - DocumentSegment.status: SegmentStatus.INDEXING, - DocumentSegment.indexing_at: naive_utc_now(), - } + session.execute( + update(DocumentSegment) + .where(DocumentSegment.id == segment.id) + .values(status=SegmentStatus.INDEXING, indexing_at=naive_utc_now()) ) session.commit() document = Document( @@ -81,11 +81,10 @@ def create_segment_to_index_task(segment_id: str, keywords: list[str] | None = N index_processor.load(dataset, [document]) # update segment to completed - session.query(DocumentSegment).filter_by(id=segment.id).update( - { - DocumentSegment.status: SegmentStatus.COMPLETED, - DocumentSegment.completed_at: naive_utc_now(), - } + session.execute( + update(DocumentSegment) + .where(DocumentSegment.id == segment.id) + .values(status=SegmentStatus.COMPLETED, completed_at=naive_utc_now()) ) session.commit() diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index ecf6f9cb39..55a99dde7a 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -1,6 +1,7 @@ import logging from celery import shared_task +from sqlalchemy import select from configs import dify_config from core.db.session_factory import session_factory @@ -14,7 +15,7 @@ logger = logging.getLogger(__name__) @shared_task(queue="dataset") def delete_account_task(account_id): with session_factory.create_session() as session: - account = session.query(Account).where(Account.id == account_id).first() + account = session.scalar(select(Account).where(Account.id == account_id).limit(1)) try: if dify_config.BILLING_ENABLED: BillingService.delete_account(account_id) diff --git a/api/tasks/disable_segment_from_index_task.py b/api/tasks/disable_segment_from_index_task.py index bc45171623..dd1a40844b 100644 --- a/api/tasks/disable_segment_from_index_task.py +++ b/api/tasks/disable_segment_from_index_task.py @@ -3,6 +3,7 @@ import time import click from celery import shared_task +from sqlalchemy import select from core.db.session_factory import session_factory from core.rag.index_processor.index_processor_factory import IndexProcessorFactory @@ -24,7 +25,7 @@ def disable_segment_from_index_task(segment_id: str): start_at = time.perf_counter() with session_factory.create_session() as session: - segment = session.query(DocumentSegment).where(DocumentSegment.id == segment_id).first() + segment = session.scalar(select(DocumentSegment).where(DocumentSegment.id == segment_id).limit(1)) if not segment: logger.info(click.style(f"Segment not found: {segment_id}", fg="red")) return diff --git a/api/tasks/document_indexing_update_task.py b/api/tasks/document_indexing_update_task.py index 62bce24de4..15f0e0162b 100644 --- a/api/tasks/document_indexing_update_task.py +++ b/api/tasks/document_indexing_update_task.py @@ -28,7 +28,9 @@ def document_indexing_update_task(dataset_id: str, document_id: str): start_at = time.perf_counter() with session_factory.create_session() as session, session.begin(): - document = session.query(Document).where(Document.id == document_id, Document.dataset_id == dataset_id).first() + document = session.scalar( + select(Document).where(Document.id == document_id, Document.dataset_id == dataset_id).limit(1) + ) if not document: logger.info(click.style(f"Document not found: {document_id}", fg="red")) @@ -37,7 +39,7 @@ def document_indexing_update_task(dataset_id: str, document_id: str): document.indexing_status = IndexingStatus.PARSING document.processing_started_at = naive_utc_now() - dataset = session.query(Dataset).where(Dataset.id == dataset_id).first() + dataset = session.scalar(select(Dataset).where(Dataset.id == dataset_id).limit(1)) if not dataset: return diff --git a/api/tasks/duplicate_document_indexing_task.py b/api/tasks/duplicate_document_indexing_task.py index 13c651753f..6bc58bdf9c 100644 --- a/api/tasks/duplicate_document_indexing_task.py +++ b/api/tasks/duplicate_document_indexing_task.py @@ -82,7 +82,7 @@ def _duplicate_document_indexing_task(dataset_id: str, document_ids: Sequence[st with session_factory.create_session() as session: try: - dataset = session.query(Dataset).where(Dataset.id == dataset_id).first() + dataset = session.scalar(select(Dataset).where(Dataset.id == dataset_id).limit(1)) if dataset is None: logger.info(click.style(f"Dataset not found: {dataset_id}", fg="red")) return diff --git a/api/tasks/enable_segment_to_index_task.py b/api/tasks/enable_segment_to_index_task.py index 5ad17d75d4..8334ca2588 100644 --- a/api/tasks/enable_segment_to_index_task.py +++ b/api/tasks/enable_segment_to_index_task.py @@ -3,6 +3,7 @@ import time import click from celery import shared_task +from sqlalchemy import select from core.db.session_factory import session_factory from core.rag.index_processor.constant.doc_type import DocType @@ -29,7 +30,7 @@ def enable_segment_to_index_task(segment_id: str): start_at = time.perf_counter() with session_factory.create_session() as session: - segment = session.query(DocumentSegment).where(DocumentSegment.id == segment_id).first() + segment = session.scalar(select(DocumentSegment).where(DocumentSegment.id == segment_id).limit(1)) if not segment: logger.info(click.style(f"Segment not found: {segment_id}", fg="red")) return diff --git a/api/tasks/enable_segments_to_index_task.py b/api/tasks/enable_segments_to_index_task.py index d90eb4c39f..603abf62fe 100644 --- a/api/tasks/enable_segments_to_index_task.py +++ b/api/tasks/enable_segments_to_index_task.py @@ -3,7 +3,7 @@ import time import click from celery import shared_task -from sqlalchemy import select +from sqlalchemy import select, update from core.db.session_factory import session_factory from core.rag.index_processor.constant.doc_type import DocType @@ -30,12 +30,12 @@ def enable_segments_to_index_task(segment_ids: list, dataset_id: str, document_i """ start_at = time.perf_counter() with session_factory.create_session() as session: - dataset = session.query(Dataset).where(Dataset.id == dataset_id).first() + dataset = session.scalar(select(Dataset).where(Dataset.id == dataset_id).limit(1)) if not dataset: logger.info(click.style(f"Dataset {dataset_id} not found, pass.", fg="cyan")) return - dataset_document = session.query(DatasetDocument).where(DatasetDocument.id == document_id).first() + dataset_document = session.scalar(select(DatasetDocument).where(DatasetDocument.id == document_id).limit(1)) if not dataset_document: logger.info(click.style(f"Document {document_id} not found, pass.", fg="cyan")) @@ -123,17 +123,14 @@ def enable_segments_to_index_task(segment_ids: list, dataset_id: str, document_i except Exception as e: logger.exception("enable segments to index failed") # update segment error msg - session.query(DocumentSegment).where( - DocumentSegment.id.in_(segment_ids), - DocumentSegment.dataset_id == dataset_id, - DocumentSegment.document_id == document_id, - ).update( - { - "error": str(e), - "status": "error", - "disabled_at": naive_utc_now(), - "enabled": False, - } + session.execute( + update(DocumentSegment) + .where( + DocumentSegment.id.in_(segment_ids), + DocumentSegment.dataset_id == dataset_id, + DocumentSegment.document_id == document_id, + ) + .values(error=str(e), status="error", disabled_at=naive_utc_now(), enabled=False) ) session.commit() finally: diff --git a/api/tasks/generate_summary_index_task.py b/api/tasks/generate_summary_index_task.py index e3d82d2851..9eda5716b8 100644 --- a/api/tasks/generate_summary_index_task.py +++ b/api/tasks/generate_summary_index_task.py @@ -5,6 +5,7 @@ import time import click from celery import shared_task +from sqlalchemy import select, update from core.db.session_factory import session_factory from core.rag.index_processor.constant.index_type import IndexTechniqueType @@ -39,12 +40,12 @@ def generate_summary_index_task(dataset_id: str, document_id: str, segment_ids: try: with session_factory.create_session() as session: - dataset = session.query(Dataset).where(Dataset.id == dataset_id).first() + dataset = session.scalar(select(Dataset).where(Dataset.id == dataset_id).limit(1)) if not dataset: logger.error(click.style(f"Dataset not found: {dataset_id}", fg="red")) return - document = session.query(DatasetDocument).where(DatasetDocument.id == document_id).first() + document = session.scalar(select(DatasetDocument).where(DatasetDocument.id == document_id).limit(1)) if not document: logger.error(click.style(f"Document not found: {document_id}", fg="red")) return @@ -108,13 +109,12 @@ def generate_summary_index_task(dataset_id: str, document_id: str, segment_ids: if segment_ids: error_message = f"Summary generation failed: {str(e)}" with session_factory.create_session() as session: - session.query(DocumentSegment).filter( - DocumentSegment.id.in_(segment_ids), - DocumentSegment.dataset_id == dataset_id, - ).update( - { - DocumentSegment.error: error_message, - }, - synchronize_session=False, + session.execute( + update(DocumentSegment) + .where( + DocumentSegment.id.in_(segment_ids), + DocumentSegment.dataset_id == dataset_id, + ) + .values(error=error_message) ) session.commit() diff --git a/api/tasks/rag_pipeline/priority_rag_pipeline_run_task.py b/api/tasks/rag_pipeline/priority_rag_pipeline_run_task.py index 3c5e152520..d8fa73b42d 100644 --- a/api/tasks/rag_pipeline/priority_rag_pipeline_run_task.py +++ b/api/tasks/rag_pipeline/priority_rag_pipeline_run_task.py @@ -10,6 +10,7 @@ from typing import Any import click from celery import shared_task # type: ignore from flask import current_app, g +from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker from configs import dify_config @@ -118,20 +119,20 @@ def run_single_rag_pipeline_task(rag_pipeline_invoke_entity: Mapping[str, Any], with Session(db.engine, expire_on_commit=False) as session: # Load required entities - account = session.query(Account).where(Account.id == user_id).first() + account = session.scalar(select(Account).where(Account.id == user_id).limit(1)) if not account: raise ValueError(f"Account {user_id} not found") - tenant = session.query(Tenant).where(Tenant.id == tenant_id).first() + tenant = session.scalar(select(Tenant).where(Tenant.id == tenant_id).limit(1)) if not tenant: raise ValueError(f"Tenant {tenant_id} not found") account.current_tenant = tenant - pipeline = session.query(Pipeline).where(Pipeline.id == pipeline_id).first() + pipeline = session.scalar(select(Pipeline).where(Pipeline.id == pipeline_id).limit(1)) if not pipeline: raise ValueError(f"Pipeline {pipeline_id} not found") - workflow = session.query(Workflow).where(Workflow.id == pipeline.workflow_id).first() + workflow = session.scalar(select(Workflow).where(Workflow.id == pipeline.workflow_id).limit(1)) if not workflow: raise ValueError(f"Workflow {pipeline.workflow_id} not found") diff --git a/api/tasks/rag_pipeline/rag_pipeline_run_task.py b/api/tasks/rag_pipeline/rag_pipeline_run_task.py index 52f66dddb8..8e1e096ed0 100644 --- a/api/tasks/rag_pipeline/rag_pipeline_run_task.py +++ b/api/tasks/rag_pipeline/rag_pipeline_run_task.py @@ -11,7 +11,8 @@ from typing import Any import click from celery import group, shared_task from flask import current_app, g -from sqlalchemy.orm import Session, sessionmaker +from sqlalchemy import select +from sqlalchemy.orm import sessionmaker from configs import dify_config from core.app.entities.app_invoke_entities import InvokeFrom, RagPipelineGenerateEntity @@ -130,22 +131,22 @@ def run_single_rag_pipeline_task(rag_pipeline_invoke_entity: Mapping[str, Any], workflow_thread_pool_id = rag_pipeline_invoke_entity_model.workflow_thread_pool_id application_generate_entity = rag_pipeline_invoke_entity_model.application_generate_entity - with Session(db.engine) as session: + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: # Load required entities - account = session.query(Account).where(Account.id == user_id).first() + account = session.scalar(select(Account).where(Account.id == user_id).limit(1)) if not account: raise ValueError(f"Account {user_id} not found") - tenant = session.query(Tenant).where(Tenant.id == tenant_id).first() + tenant = session.scalar(select(Tenant).where(Tenant.id == tenant_id).limit(1)) if not tenant: raise ValueError(f"Tenant {tenant_id} not found") account.current_tenant = tenant - pipeline = session.query(Pipeline).where(Pipeline.id == pipeline_id).first() + pipeline = session.scalar(select(Pipeline).where(Pipeline.id == pipeline_id).limit(1)) if not pipeline: raise ValueError(f"Pipeline {pipeline_id} not found") - workflow = session.query(Workflow).where(Workflow.id == pipeline.workflow_id).first() + workflow = session.scalar(select(Workflow).where(Workflow.id == pipeline.workflow_id).limit(1)) if not workflow: raise ValueError(f"Workflow {pipeline.workflow_id} not found") diff --git a/api/tasks/recover_document_indexing_task.py b/api/tasks/recover_document_indexing_task.py index af72023da1..73b121961c 100644 --- a/api/tasks/recover_document_indexing_task.py +++ b/api/tasks/recover_document_indexing_task.py @@ -3,6 +3,7 @@ import time import click from celery import shared_task +from sqlalchemy import select from core.db.session_factory import session_factory from core.indexing_runner import DocumentIsPausedError, IndexingRunner @@ -24,7 +25,9 @@ def recover_document_indexing_task(dataset_id: str, document_id: str): start_at = time.perf_counter() with session_factory.create_session() as session: - document = session.query(Document).where(Document.id == document_id, Document.dataset_id == dataset_id).first() + document = session.scalar( + select(Document).where(Document.id == document_id, Document.dataset_id == dataset_id).limit(1) + ) if not document: logger.info(click.style(f"Document not found: {document_id}", fg="red")) diff --git a/api/tasks/remove_document_from_index_task.py b/api/tasks/remove_document_from_index_task.py index 55259ab527..74e8a012cf 100644 --- a/api/tasks/remove_document_from_index_task.py +++ b/api/tasks/remove_document_from_index_task.py @@ -3,7 +3,7 @@ import time import click from celery import shared_task -from sqlalchemy import select +from sqlalchemy import select, update from core.db.session_factory import session_factory from core.rag.index_processor.index_processor_factory import IndexProcessorFactory @@ -26,7 +26,7 @@ def remove_document_from_index_task(document_id: str): start_at = time.perf_counter() with session_factory.create_session() as session: - document = session.query(Document).where(Document.id == document_id).first() + document = session.scalar(select(Document).where(Document.id == document_id).limit(1)) if not document: logger.info(click.style(f"Document not found: {document_id}", fg="red")) return @@ -68,13 +68,15 @@ def remove_document_from_index_task(document_id: str): except Exception: logger.exception("clean dataset %s from index failed", dataset.id) # update segment to disable - session.query(DocumentSegment).where(DocumentSegment.document_id == document.id).update( - { - DocumentSegment.enabled: False, - DocumentSegment.disabled_at: naive_utc_now(), - DocumentSegment.disabled_by: document.disabled_by, - DocumentSegment.updated_at: naive_utc_now(), - } + session.execute( + update(DocumentSegment) + .where(DocumentSegment.document_id == document.id) + .values( + enabled=False, + disabled_at=naive_utc_now(), + disabled_by=document.disabled_by, + updated_at=naive_utc_now(), + ) ) session.commit() diff --git a/api/tasks/retry_document_indexing_task.py b/api/tasks/retry_document_indexing_task.py index 4fcb0cf804..7cc28d5226 100644 --- a/api/tasks/retry_document_indexing_task.py +++ b/api/tasks/retry_document_indexing_task.py @@ -32,15 +32,15 @@ def retry_document_indexing_task(dataset_id: str, document_ids: list[str], user_ start_at = time.perf_counter() with session_factory.create_session() as session: try: - dataset = session.query(Dataset).where(Dataset.id == dataset_id).first() + dataset = session.scalar(select(Dataset).where(Dataset.id == dataset_id).limit(1)) if not dataset: logger.info(click.style(f"Dataset not found: {dataset_id}", fg="red")) return - user = session.query(Account).where(Account.id == user_id).first() + user = session.scalar(select(Account).where(Account.id == user_id).limit(1)) if not user: logger.info(click.style(f"User not found: {user_id}", fg="red")) return - tenant = session.query(Tenant).where(Tenant.id == dataset.tenant_id).first() + tenant = session.scalar(select(Tenant).where(Tenant.id == dataset.tenant_id).limit(1)) if not tenant: raise ValueError("Tenant not found") user.current_tenant = tenant @@ -58,10 +58,8 @@ def retry_document_indexing_task(dataset_id: str, document_ids: list[str], user_ "your subscription." ) except Exception as e: - document = ( - session.query(Document) - .where(Document.id == document_id, Document.dataset_id == dataset_id) - .first() + document = session.scalar( + select(Document).where(Document.id == document_id, Document.dataset_id == dataset_id).limit(1) ) if document: document.indexing_status = IndexingStatus.ERROR @@ -73,8 +71,8 @@ def retry_document_indexing_task(dataset_id: str, document_ids: list[str], user_ return logger.info(click.style(f"Start retry document: {document_id}", fg="green")) - document = ( - session.query(Document).where(Document.id == document_id, Document.dataset_id == dataset_id).first() + document = session.scalar( + select(Document).where(Document.id == document_id, Document.dataset_id == dataset_id).limit(1) ) if not document: logger.info(click.style(f"Document not found: {document_id}", fg="yellow")) diff --git a/api/tasks/sync_website_document_indexing_task.py b/api/tasks/sync_website_document_indexing_task.py index aa6bce958b..ab21f63f7e 100644 --- a/api/tasks/sync_website_document_indexing_task.py +++ b/api/tasks/sync_website_document_indexing_task.py @@ -29,7 +29,7 @@ def sync_website_document_indexing_task(dataset_id: str, document_id: str): start_at = time.perf_counter() with session_factory.create_session() as session: - dataset = session.query(Dataset).where(Dataset.id == dataset_id).first() + dataset = session.scalar(select(Dataset).where(Dataset.id == dataset_id).limit(1)) if dataset is None: raise ValueError("Dataset not found") @@ -45,8 +45,8 @@ def sync_website_document_indexing_task(dataset_id: str, document_id: str): "your subscription." ) except Exception as e: - document = ( - session.query(Document).where(Document.id == document_id, Document.dataset_id == dataset_id).first() + document = session.scalar( + select(Document).where(Document.id == document_id, Document.dataset_id == dataset_id).limit(1) ) if document: document.indexing_status = IndexingStatus.ERROR @@ -58,7 +58,9 @@ def sync_website_document_indexing_task(dataset_id: str, document_id: str): return logger.info(click.style(f"Start sync website document: {document_id}", fg="green")) - document = session.query(Document).where(Document.id == document_id, Document.dataset_id == dataset_id).first() + document = session.scalar( + select(Document).where(Document.id == document_id, Document.dataset_id == dataset_id).limit(1) + ) if not document: logger.info(click.style(f"Document not found: {document_id}", fg="yellow")) return diff --git a/api/tasks/trigger_subscription_refresh_tasks.py b/api/tasks/trigger_subscription_refresh_tasks.py index 7698a1a6b8..1daf8f302c 100644 --- a/api/tasks/trigger_subscription_refresh_tasks.py +++ b/api/tasks/trigger_subscription_refresh_tasks.py @@ -4,6 +4,7 @@ from collections.abc import Mapping from typing import Any from celery import shared_task +from sqlalchemy import select from sqlalchemy.orm import Session from configs import dify_config @@ -22,7 +23,11 @@ def _now_ts() -> int: def _load_subscription(session: Session, tenant_id: str, subscription_id: str) -> TriggerSubscription | None: - return session.query(TriggerSubscription).filter_by(tenant_id=tenant_id, id=subscription_id).first() + return session.scalar( + select(TriggerSubscription) + .where(TriggerSubscription.tenant_id == tenant_id, TriggerSubscription.id == subscription_id) + .limit(1) + ) def _refresh_oauth_if_expired(tenant_id: str, subscription: TriggerSubscription, now: int) -> None: diff --git a/api/tests/test_containers_integration_tests/controllers/console/app/test_app_apis.py b/api/tests/test_containers_integration_tests/controllers/console/app/test_app_apis.py index 0841217fcf..c3a861c3e1 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/app/test_app_apis.py +++ b/api/tests/test_containers_integration_tests/controllers/console/app/test_app_apis.py @@ -555,7 +555,7 @@ class TestWorkflowTriggerEndpoints: trigger = MagicMock() session = MagicMock() - session.query.return_value.where.return_value.first.return_value = trigger + session.scalar.return_value = trigger class DummySessionCtx: def __enter__(self): diff --git a/api/tests/test_containers_integration_tests/controllers/mcp/test_mcp.py b/api/tests/test_containers_integration_tests/controllers/mcp/test_mcp.py index 90670a9db5..21b395a04c 100644 --- a/api/tests/test_containers_integration_tests/controllers/mcp/test_mcp.py +++ b/api/tests/test_containers_integration_tests/controllers/mcp/test_mcp.py @@ -444,7 +444,7 @@ class TestMCPAppApi: ) session = MagicMock() - session.query().where().first.side_effect = [server, app] + session.scalar.side_effect = [server, app] result_server, result_app = api._get_mcp_server_and_app("server-1", session) diff --git a/web/app/components/base/auto-height-textarea/style.module.scss b/api/tests/test_containers_integration_tests/controllers/service_api/__init__.py similarity index 100% rename from web/app/components/base/auto-height-textarea/style.module.scss rename to api/tests/test_containers_integration_tests/controllers/service_api/__init__.py diff --git a/api/tests/test_containers_integration_tests/controllers/service_api/dataset/__init__.py b/api/tests/test_containers_integration_tests/controllers/service_api/dataset/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/unit_tests/controllers/service_api/dataset/test_dataset.py b/api/tests/test_containers_integration_tests/controllers/service_api/dataset/test_dataset.py similarity index 50% rename from api/tests/unit_tests/controllers/service_api/dataset/test_dataset.py rename to api/tests/test_containers_integration_tests/controllers/service_api/dataset/test_dataset.py index 910d781cd0..9b913d6d3d 100644 --- a/api/tests/unit_tests/controllers/service_api/dataset/test_dataset.py +++ b/api/tests/test_containers_integration_tests/controllers/service_api/dataset/test_dataset.py @@ -1,17 +1,16 @@ """ -Unit tests for Service API Dataset controllers. +Integration tests for Service API Dataset controllers. + +Migrated from unit_tests/controllers/service_api/dataset/test_dataset.py. Tests coverage for: - DatasetCreatePayload, DatasetUpdatePayload Pydantic models - Tag-related payloads (create, update, delete, binding) - DatasetListQuery model -- DatasetService and TagService interfaces -- Permission validation patterns +- API endpoint error handling and controller behavior -Focus on: -- Pydantic model validation -- Error type mappings -- Service method interfaces +Services (DatasetService, TagService, DocumentService) remain mocked +since these test controller-level behavior. """ import uuid @@ -19,6 +18,7 @@ from types import SimpleNamespace from unittest.mock import Mock, patch import pytest +from sqlalchemy.orm import Session from werkzeug.exceptions import Forbidden, NotFound import services @@ -36,22 +36,23 @@ from controllers.service_api.dataset.error import DatasetInUseError, DatasetName from models.account import Account from models.dataset import DatasetPermissionEnum from models.enums import TagType -from services.dataset_service import DatasetPermissionService, DatasetService, DocumentService -from services.tag_service import TagService +from models.model import Tag + +# --------------------------------------------------------------------------- +# Pydantic model validation tests +# --------------------------------------------------------------------------- class TestDatasetCreatePayload: """Test suite for DatasetCreatePayload Pydantic model.""" def test_payload_with_required_name(self): - """Test payload with required name field.""" payload = DatasetCreatePayload(name="Test Dataset") assert payload.name == "Test Dataset" assert payload.description == "" assert payload.permission == DatasetPermissionEnum.ONLY_ME def test_payload_with_all_fields(self): - """Test payload with all fields populated.""" payload = DatasetCreatePayload( name="Full Dataset", description="A comprehensive dataset description", @@ -70,28 +71,23 @@ class TestDatasetCreatePayload: assert payload.embedding_model_provider == "openai" def test_payload_name_length_validation_min(self): - """Test name minimum length validation.""" with pytest.raises(ValueError): DatasetCreatePayload(name="") def test_payload_name_length_validation_max(self): - """Test name maximum length validation (40 chars).""" with pytest.raises(ValueError): DatasetCreatePayload(name="A" * 41) def test_payload_description_max_length(self): - """Test description maximum length (400 chars).""" with pytest.raises(ValueError): DatasetCreatePayload(name="Dataset", description="A" * 401) @pytest.mark.parametrize("technique", ["high_quality", "economy"]) def test_payload_valid_indexing_techniques(self, technique): - """Test valid indexing technique values.""" payload = DatasetCreatePayload(name="Dataset", indexing_technique=technique) assert payload.indexing_technique == technique def test_payload_with_external_knowledge_settings(self): - """Test payload with external knowledge configuration.""" payload = DatasetCreatePayload( name="External Dataset", external_knowledge_api_id="api_123", external_knowledge_id="knowledge_456" ) @@ -103,20 +99,17 @@ class TestDatasetUpdatePayload: """Test suite for DatasetUpdatePayload Pydantic model.""" def test_payload_all_optional(self): - """Test payload with all fields optional.""" payload = DatasetUpdatePayload() assert payload.name is None assert payload.description is None assert payload.permission is None def test_payload_with_partial_update(self): - """Test payload with partial update fields.""" payload = DatasetUpdatePayload(name="Updated Name", description="Updated description") assert payload.name == "Updated Name" assert payload.description == "Updated description" def test_payload_with_permission_change(self): - """Test payload with permission update.""" payload = DatasetUpdatePayload( permission=DatasetPermissionEnum.PARTIAL_TEAM, partial_member_list=[{"user_id": "user_123", "role": "editor"}], @@ -125,12 +118,8 @@ class TestDatasetUpdatePayload: assert len(payload.partial_member_list) == 1 def test_payload_name_length_validation(self): - """Test name length constraints.""" - # Minimum is 1 with pytest.raises(ValueError): DatasetUpdatePayload(name="") - - # Maximum is 40 with pytest.raises(ValueError): DatasetUpdatePayload(name="A" * 41) @@ -139,7 +128,6 @@ class TestDatasetListQuery: """Test suite for DatasetListQuery Pydantic model.""" def test_query_with_defaults(self): - """Test query with default values.""" query = DatasetListQuery() assert query.page == 1 assert query.limit == 20 @@ -148,7 +136,6 @@ class TestDatasetListQuery: assert query.tag_ids == [] def test_query_with_all_filters(self): - """Test query with all filter fields.""" query = DatasetListQuery( page=3, limit=50, keyword="machine learning", include_all=True, tag_ids=["tag1", "tag2", "tag3"] ) @@ -159,7 +146,6 @@ class TestDatasetListQuery: assert len(query.tag_ids) == 3 def test_query_with_tag_filter(self): - """Test query with tag IDs filter.""" query = DatasetListQuery(tag_ids=["tag_abc", "tag_def"]) assert query.tag_ids == ["tag_abc", "tag_def"] @@ -168,22 +154,18 @@ class TestTagCreatePayload: """Test suite for TagCreatePayload Pydantic model.""" def test_payload_with_name(self): - """Test payload with required name.""" payload = TagCreatePayload(name="New Tag") assert payload.name == "New Tag" def test_payload_name_length_min(self): - """Test name minimum length (1).""" with pytest.raises(ValueError): TagCreatePayload(name="") def test_payload_name_length_max(self): - """Test name maximum length (50).""" with pytest.raises(ValueError): TagCreatePayload(name="A" * 51) def test_payload_with_unicode_name(self): - """Test payload with unicode characters.""" payload = TagCreatePayload(name="标签 🏷️ Тег") assert payload.name == "标签 🏷️ Тег" @@ -192,13 +174,11 @@ class TestTagUpdatePayload: """Test suite for TagUpdatePayload Pydantic model.""" def test_payload_with_name_and_id(self): - """Test payload with name and tag_id.""" payload = TagUpdatePayload(name="Updated Tag", tag_id="tag_123") assert payload.name == "Updated Tag" assert payload.tag_id == "tag_123" def test_payload_requires_tag_id(self): - """Test that tag_id is required.""" with pytest.raises(ValueError): TagUpdatePayload(name="Updated Tag") @@ -207,12 +187,10 @@ class TestTagDeletePayload: """Test suite for TagDeletePayload Pydantic model.""" def test_payload_with_tag_id(self): - """Test payload with tag_id.""" payload = TagDeletePayload(tag_id="tag_to_delete") assert payload.tag_id == "tag_to_delete" def test_payload_requires_tag_id(self): - """Test that tag_id is required.""" with pytest.raises(ValueError): TagDeletePayload() @@ -221,19 +199,16 @@ class TestTagBindingPayload: """Test suite for TagBindingPayload Pydantic model.""" def test_payload_with_valid_data(self): - """Test payload with valid binding data.""" payload = TagBindingPayload(tag_ids=["tag1", "tag2"], target_id="dataset_123") assert len(payload.tag_ids) == 2 assert payload.target_id == "dataset_123" def test_payload_rejects_empty_tag_ids(self): - """Test that empty tag_ids are rejected.""" with pytest.raises(ValueError) as exc_info: TagBindingPayload(tag_ids=[], target_id="dataset_123") assert "Tag IDs is required" in str(exc_info.value) def test_payload_single_tag_id(self): - """Test payload with single tag ID.""" payload = TagBindingPayload(tag_ids=["single_tag"], target_id="dataset_456") assert payload.tag_ids == ["single_tag"] @@ -242,674 +217,14 @@ class TestTagUnbindingPayload: """Test suite for TagUnbindingPayload Pydantic model.""" def test_payload_with_valid_data(self): - """Test payload with valid unbinding data.""" payload = TagUnbindingPayload(tag_id="tag_123", target_id="dataset_456") assert payload.tag_id == "tag_123" assert payload.target_id == "dataset_456" -class TestDatasetTagsApi: - """Test suite for DatasetTagsApi endpoints.""" - - @pytest.fixture - def app(self): - """Create Flask test application.""" - from flask import Flask - - app = Flask(__name__) - app.config["TESTING"] = True - return app - - @patch("controllers.service_api.dataset.dataset.current_user") - @patch("controllers.service_api.dataset.dataset.TagService") - def test_get_tags_success(self, mock_tag_service, mock_current_user, app): - """Test successful retrieval of dataset tags.""" - # Arrange - mock_current_user needs to pass isinstance(current_user, Account) - from models.account import Account - - mock_account = Mock(spec=Account) - mock_account.current_tenant_id = "tenant_123" - # Replace the mock with our properly specced one - from controllers.service_api.dataset import dataset as dataset_module - - original_current_user = dataset_module.current_user - dataset_module.current_user = mock_account - - mock_tag = Mock() - mock_tag.id = "tag_1" - mock_tag.name = "Test Tag" - mock_tag.type = TagType.KNOWLEDGE - mock_tag.binding_count = "0" # Required for Pydantic validation - must be string - mock_tag_service.get_tags.return_value = [mock_tag] - - from controllers.service_api.dataset.dataset import DatasetTagsApi - - try: - # Act - with app.test_request_context("/", method="GET"): - api = DatasetTagsApi() - response, status_code = api.get("tenant_123") - - # Assert - assert status_code == 200 - assert len(response) == 1 - assert response[0]["id"] == "tag_1" - assert response[0]["name"] == "Test Tag" - mock_tag_service.get_tags.assert_called_once_with("knowledge", "tenant_123") - finally: - dataset_module.current_user = original_current_user - - @pytest.mark.skip(reason="Production code bug: binding_count should be string, not integer") - @patch("controllers.service_api.dataset.dataset.TagService") - @patch("controllers.service_api.dataset.dataset.service_api_ns") - def test_create_tag_success(self, mock_service_api_ns, mock_tag_service, app): - """Test successful creation of a dataset tag.""" - # Arrange - from controllers.service_api.dataset import dataset as dataset_module - from models.account import Account - - mock_account = Mock(spec=Account) - mock_account.has_edit_permission = True - mock_account.is_dataset_editor = False - original_current_user = dataset_module.current_user - dataset_module.current_user = mock_account - - mock_tag = Mock() - mock_tag.id = "new_tag_1" - mock_tag.name = "New Tag" - mock_tag.type = TagType.KNOWLEDGE - mock_tag_service.save_tags.return_value = mock_tag - mock_service_api_ns.payload = {"name": "New Tag"} - - from controllers.service_api.dataset.dataset import DatasetTagsApi - - try: - # Act - with app.test_request_context("/", method="POST", json={"name": "New Tag"}): - api = DatasetTagsApi() - response, status_code = api.post("tenant_123") - - # Assert - assert status_code == 200 - assert response["id"] == "new_tag_1" - assert response["name"] == "New Tag" - assert response["binding_count"] == 0 - finally: - dataset_module.current_user = original_current_user - - def test_create_tag_forbidden(self, app): - """Test tag creation without edit permissions.""" - # Arrange - from werkzeug.exceptions import Forbidden - - from controllers.service_api.dataset import dataset as dataset_module - from models.account import Account - - mock_account = Mock(spec=Account) - mock_account.has_edit_permission = False - mock_account.is_dataset_editor = False - original_current_user = dataset_module.current_user - dataset_module.current_user = mock_account - - from controllers.service_api.dataset.dataset import DatasetTagsApi - - try: - # Act & Assert - with app.test_request_context("/", method="POST"): - api = DatasetTagsApi() - with pytest.raises(Forbidden): - api.post("tenant_123") - finally: - dataset_module.current_user = original_current_user - - @pytest.mark.skip(reason="Production code bug: binding_count should be string, not integer") - @patch("controllers.service_api.dataset.dataset.TagService") - @patch("controllers.service_api.dataset.dataset.service_api_ns") - def test_update_tag_success(self, mock_service_api_ns, mock_tag_service, app): - """Test successful update of a dataset tag.""" - # Arrange - from controllers.service_api.dataset import dataset as dataset_module - from models.account import Account - - mock_account = Mock(spec=Account) - mock_account.has_edit_permission = True - original_current_user = dataset_module.current_user - dataset_module.current_user = mock_account - - mock_tag = Mock() - mock_tag.id = "tag_1" - mock_tag.name = "Updated Tag" - mock_tag.type = TagType.KNOWLEDGE - mock_tag.binding_count = "5" - mock_tag_service.update_tags.return_value = mock_tag - mock_tag_service.get_tag_binding_count.return_value = 5 - mock_service_api_ns.payload = {"name": "Updated Tag", "tag_id": "tag_1"} - - from controllers.service_api.dataset.dataset import DatasetTagsApi - - try: - # Act - with app.test_request_context("/", method="PATCH", json={"name": "Updated Tag", "tag_id": "tag_1"}): - api = DatasetTagsApi() - response, status_code = api.patch("tenant_123") - - # Assert - assert status_code == 200 - assert response["id"] == "tag_1" - assert response["name"] == "Updated Tag" - assert response["binding_count"] == 5 - finally: - dataset_module.current_user = original_current_user - - @pytest.mark.skip(reason="Production code bug: binding_count should be string, not integer") - @patch("controllers.service_api.dataset.dataset.TagService") - @patch("controllers.service_api.dataset.dataset.service_api_ns") - def test_delete_tag_success(self, mock_service_api_ns, mock_tag_service, app): - """Test successful deletion of a dataset tag.""" - # Arrange - from controllers.service_api.dataset import dataset as dataset_module - from models.account import Account - - mock_account = Mock(spec=Account) - mock_account.has_edit_permission = True - original_current_user = dataset_module.current_user - dataset_module.current_user = mock_account - - mock_tag_service.delete_tag.return_value = None - mock_service_api_ns.payload = {"tag_id": "tag_1"} - - from controllers.service_api.dataset.dataset import DatasetTagsApi - - try: - # Act - with app.test_request_context("/", method="DELETE", json={"tag_id": "tag_1"}): - api = DatasetTagsApi() - response = api.delete("tenant_123") - - # Assert - assert response == ("", 204) - mock_tag_service.delete_tag.assert_called_once_with("tag_1") - finally: - dataset_module.current_user = original_current_user - - -class TestDatasetTagBindingApi: - """Test suite for DatasetTagBindingApi endpoints.""" - - @pytest.fixture - def app(self): - """Create Flask test application.""" - from flask import Flask - - app = Flask(__name__) - app.config["TESTING"] = True - return app - - @patch("controllers.service_api.dataset.dataset.TagService") - @patch("controllers.service_api.dataset.dataset.service_api_ns") - def test_bind_tags_success(self, mock_service_api_ns, mock_tag_service, app): - """Test successful binding of tags to dataset.""" - # Arrange - from controllers.service_api.dataset import dataset as dataset_module - from models.account import Account - - mock_account = Mock(spec=Account) - mock_account.has_edit_permission = True - mock_account.is_dataset_editor = False - original_current_user = dataset_module.current_user - dataset_module.current_user = mock_account - - mock_tag_service.save_tag_binding.return_value = None - payload = {"tag_ids": ["tag_1", "tag_2"], "target_id": "dataset_123"} - mock_service_api_ns.payload = payload - - from controllers.service_api.dataset.dataset import DatasetTagBindingApi - - try: - # Act - with app.test_request_context("/", method="POST", json=payload): - api = DatasetTagBindingApi() - response = api.post("tenant_123") - - # Assert - assert response == ("", 204) - mock_tag_service.save_tag_binding.assert_called_once_with( - {"tag_ids": ["tag_1", "tag_2"], "target_id": "dataset_123", "type": "knowledge"} - ) - finally: - dataset_module.current_user = original_current_user - - def test_bind_tags_forbidden(self, app): - """Test tag binding without edit permissions.""" - # Arrange - from werkzeug.exceptions import Forbidden - - from controllers.service_api.dataset import dataset as dataset_module - from models.account import Account - - mock_account = Mock(spec=Account) - mock_account.has_edit_permission = False - mock_account.is_dataset_editor = False - original_current_user = dataset_module.current_user - dataset_module.current_user = mock_account - - from controllers.service_api.dataset.dataset import DatasetTagBindingApi - - try: - # Act & Assert - with app.test_request_context("/", method="POST"): - api = DatasetTagBindingApi() - with pytest.raises(Forbidden): - api.post("tenant_123") - finally: - dataset_module.current_user = original_current_user - - -class TestDatasetTagUnbindingApi: - """Test suite for DatasetTagUnbindingApi endpoints.""" - - @pytest.fixture - def app(self): - """Create Flask test application.""" - from flask import Flask - - app = Flask(__name__) - app.config["TESTING"] = True - return app - - @patch("controllers.service_api.dataset.dataset.TagService") - @patch("controllers.service_api.dataset.dataset.service_api_ns") - def test_unbind_tag_success(self, mock_service_api_ns, mock_tag_service, app): - """Test successful unbinding of tag from dataset.""" - # Arrange - from controllers.service_api.dataset import dataset as dataset_module - from models.account import Account - - mock_account = Mock(spec=Account) - mock_account.has_edit_permission = True - mock_account.is_dataset_editor = False - original_current_user = dataset_module.current_user - dataset_module.current_user = mock_account - - mock_tag_service.delete_tag_binding.return_value = None - payload = {"tag_id": "tag_1", "target_id": "dataset_123"} - mock_service_api_ns.payload = payload - - from controllers.service_api.dataset.dataset import DatasetTagUnbindingApi - - try: - # Act - with app.test_request_context("/", method="POST", json=payload): - api = DatasetTagUnbindingApi() - response = api.post("tenant_123") - - # Assert - assert response == ("", 204) - mock_tag_service.delete_tag_binding.assert_called_once_with( - {"tag_id": "tag_1", "target_id": "dataset_123", "type": "knowledge"} - ) - finally: - dataset_module.current_user = original_current_user - - -class TestDatasetTagsBindingStatusApi: - """Test suite for DatasetTagsBindingStatusApi endpoints.""" - - @pytest.fixture - def app(self): - """Create Flask test application.""" - from flask import Flask - - app = Flask(__name__) - app.config["TESTING"] = True - return app - - @patch("controllers.service_api.dataset.dataset.TagService") - def test_get_dataset_tags_binding_status(self, mock_tag_service, app): - """Test retrieval of tags bound to a specific dataset.""" - # Arrange - from controllers.service_api.dataset import dataset as dataset_module - from models.account import Account - - mock_account = Mock(spec=Account) - mock_account.current_tenant_id = "tenant_123" - original_current_user = dataset_module.current_user - dataset_module.current_user = mock_account - - mock_tag = Mock() - mock_tag.id = "tag_1" - mock_tag.name = "Test Tag" - mock_tag_service.get_tags_by_target_id.return_value = [mock_tag] - - from controllers.service_api.dataset.dataset import DatasetTagsBindingStatusApi - - try: - # Act - with app.test_request_context("/", method="GET"): - api = DatasetTagsBindingStatusApi() - response, status_code = api.get("tenant_123", dataset_id="dataset_123") - - # Assert - assert status_code == 200 - assert response["data"] == [{"id": "tag_1", "name": "Test Tag"}] - assert response["total"] == 1 - mock_tag_service.get_tags_by_target_id.assert_called_once_with("knowledge", "tenant_123", "dataset_123") - finally: - dataset_module.current_user = original_current_user - - -class TestDocumentStatusApi: - """Test suite for DocumentStatusApi batch operations.""" - - @pytest.fixture - def app(self): - """Create Flask test application.""" - from flask import Flask - - app = Flask(__name__) - app.config["TESTING"] = True - return app - - @patch("controllers.service_api.dataset.dataset.DatasetService") - @patch("controllers.service_api.dataset.dataset.DocumentService") - def test_batch_enable_documents(self, mock_doc_service, mock_dataset_service, app): - """Test batch enabling documents.""" - # Arrange - mock_dataset = Mock() - mock_dataset_service.get_dataset.return_value = mock_dataset - mock_doc_service.batch_update_document_status.return_value = None - - from controllers.service_api.dataset.dataset import DocumentStatusApi - - # Act - with app.test_request_context("/", method="PATCH", json={"document_ids": ["doc_1", "doc_2"]}): - api = DocumentStatusApi() - response, status_code = api.patch("tenant_123", "dataset_123", "enable") - - # Assert - assert status_code == 200 - assert response == {"result": "success"} - mock_doc_service.batch_update_document_status.assert_called_once() - - @patch("controllers.service_api.dataset.dataset.DatasetService") - def test_batch_update_dataset_not_found(self, mock_dataset_service, app): - """Test batch update when dataset not found.""" - # Arrange - mock_dataset_service.get_dataset.return_value = None - - from werkzeug.exceptions import NotFound - - from controllers.service_api.dataset.dataset import DocumentStatusApi - - # Act & Assert - with app.test_request_context("/", method="PATCH", json={"document_ids": ["doc_1"]}): - api = DocumentStatusApi() - with pytest.raises(NotFound) as exc_info: - api.patch("tenant_123", "dataset_123", "enable") - assert "Dataset not found" in str(exc_info.value) - - @patch("controllers.service_api.dataset.dataset.DatasetService") - @patch("controllers.service_api.dataset.dataset.DocumentService") - def test_batch_update_permission_error(self, mock_doc_service, mock_dataset_service, app): - """Test batch update with permission error.""" - # Arrange - mock_dataset = Mock() - mock_dataset_service.get_dataset.return_value = mock_dataset - from services.errors.account import NoPermissionError - - mock_dataset_service.check_dataset_permission.side_effect = NoPermissionError("No permission") - - from werkzeug.exceptions import Forbidden - - from controllers.service_api.dataset.dataset import DocumentStatusApi - - # Act & Assert - with app.test_request_context("/", method="PATCH", json={"document_ids": ["doc_1"]}): - api = DocumentStatusApi() - with pytest.raises(Forbidden): - api.patch("tenant_123", "dataset_123", "enable") - - @patch("controllers.service_api.dataset.dataset.DatasetService") - @patch("controllers.service_api.dataset.dataset.DocumentService") - def test_batch_update_invalid_action(self, mock_doc_service, mock_dataset_service, app): - """Test batch update with invalid action error.""" - # Arrange - mock_dataset = Mock() - mock_dataset_service.get_dataset.return_value = mock_dataset - mock_doc_service.batch_update_document_status.side_effect = ValueError("Invalid action") - - from controllers.service_api.dataset.dataset import DocumentStatusApi - from controllers.service_api.dataset.error import InvalidActionError - - # Act & Assert - with app.test_request_context("/", method="PATCH", json={"document_ids": ["doc_1"]}): - api = DocumentStatusApi() - with pytest.raises(InvalidActionError): - api.patch("tenant_123", "dataset_123", "invalid_action") - - """Test DatasetPermissionEnum values.""" - - def test_only_me_permission(self): - """Test ONLY_ME permission value.""" - assert DatasetPermissionEnum.ONLY_ME is not None - - def test_all_team_permission(self): - """Test ALL_TEAM permission value.""" - assert DatasetPermissionEnum.ALL_TEAM is not None - - def test_partial_team_permission(self): - """Test PARTIAL_TEAM permission value.""" - assert DatasetPermissionEnum.PARTIAL_TEAM is not None - - -class TestDatasetErrors: - """Test dataset-related error types.""" - - def test_dataset_in_use_error_can_be_raised(self): - """Test DatasetInUseError can be raised.""" - error = DatasetInUseError() - assert error is not None - - def test_dataset_name_duplicate_error_can_be_raised(self): - """Test DatasetNameDuplicateError can be raised.""" - error = DatasetNameDuplicateError() - assert error is not None - - def test_invalid_action_error_can_be_raised(self): - """Test InvalidActionError can be raised.""" - error = InvalidActionError("Invalid action") - assert error is not None - - -class TestDatasetService: - """Test DatasetService interface methods.""" - - def test_get_datasets_method_exists(self): - """Test DatasetService.get_datasets exists.""" - assert hasattr(DatasetService, "get_datasets") - - def test_get_dataset_method_exists(self): - """Test DatasetService.get_dataset exists.""" - assert hasattr(DatasetService, "get_dataset") - - def test_create_empty_dataset_method_exists(self): - """Test DatasetService.create_empty_dataset exists.""" - assert hasattr(DatasetService, "create_empty_dataset") - - def test_update_dataset_method_exists(self): - """Test DatasetService.update_dataset exists.""" - assert hasattr(DatasetService, "update_dataset") - - def test_delete_dataset_method_exists(self): - """Test DatasetService.delete_dataset exists.""" - assert hasattr(DatasetService, "delete_dataset") - - def test_check_dataset_permission_method_exists(self): - """Test DatasetService.check_dataset_permission exists.""" - assert hasattr(DatasetService, "check_dataset_permission") - - def test_check_dataset_model_setting_method_exists(self): - """Test DatasetService.check_dataset_model_setting exists.""" - assert hasattr(DatasetService, "check_dataset_model_setting") - - def test_check_embedding_model_setting_method_exists(self): - """Test DatasetService.check_embedding_model_setting exists.""" - assert hasattr(DatasetService, "check_embedding_model_setting") - - @patch.object(DatasetService, "get_datasets") - def test_get_datasets_returns_tuple(self, mock_get): - """Test get_datasets returns tuple of datasets and total.""" - mock_datasets = [Mock(), Mock()] - mock_get.return_value = (mock_datasets, 2) - - datasets, total = DatasetService.get_datasets(page=1, per_page=20, tenant_id="tenant_123", user=Mock()) - assert len(datasets) == 2 - assert total == 2 - - @patch.object(DatasetService, "get_dataset") - def test_get_dataset_returns_dataset(self, mock_get): - """Test get_dataset returns dataset object.""" - mock_dataset = Mock() - mock_dataset.id = str(uuid.uuid4()) - mock_dataset.name = "Test Dataset" - mock_get.return_value = mock_dataset - - result = DatasetService.get_dataset("dataset_id") - assert result.name == "Test Dataset" - - @patch.object(DatasetService, "get_dataset") - def test_get_dataset_returns_none_when_not_found(self, mock_get): - """Test get_dataset returns None when not found.""" - mock_get.return_value = None - - result = DatasetService.get_dataset("nonexistent_id") - assert result is None - - -class TestDatasetPermissionService: - """Test DatasetPermissionService interface.""" - - def test_check_permission_method_exists(self): - """Test DatasetPermissionService.check_permission exists.""" - assert hasattr(DatasetPermissionService, "check_permission") - - def test_get_dataset_partial_member_list_method_exists(self): - """Test DatasetPermissionService.get_dataset_partial_member_list exists.""" - assert hasattr(DatasetPermissionService, "get_dataset_partial_member_list") - - def test_update_partial_member_list_method_exists(self): - """Test DatasetPermissionService.update_partial_member_list exists.""" - assert hasattr(DatasetPermissionService, "update_partial_member_list") - - def test_clear_partial_member_list_method_exists(self): - """Test DatasetPermissionService.clear_partial_member_list exists.""" - assert hasattr(DatasetPermissionService, "clear_partial_member_list") - - -class TestDocumentService: - """Test DocumentService interface.""" - - def test_batch_update_document_status_method_exists(self): - """Test DocumentService.batch_update_document_status exists.""" - assert hasattr(DocumentService, "batch_update_document_status") - - -class TestTagService: - """Test TagService interface.""" - - def test_get_tags_method_exists(self): - """Test TagService.get_tags exists.""" - assert hasattr(TagService, "get_tags") - - def test_save_tags_method_exists(self): - """Test TagService.save_tags exists.""" - assert hasattr(TagService, "save_tags") - - def test_update_tags_method_exists(self): - """Test TagService.update_tags exists.""" - assert hasattr(TagService, "update_tags") - - def test_delete_tag_method_exists(self): - """Test TagService.delete_tag exists.""" - assert hasattr(TagService, "delete_tag") - - def test_save_tag_binding_method_exists(self): - """Test TagService.save_tag_binding exists.""" - assert hasattr(TagService, "save_tag_binding") - - def test_delete_tag_binding_method_exists(self): - """Test TagService.delete_tag_binding exists.""" - assert hasattr(TagService, "delete_tag_binding") - - def test_get_tags_by_target_id_method_exists(self): - """Test TagService.get_tags_by_target_id exists.""" - assert hasattr(TagService, "get_tags_by_target_id") - - def test_get_tag_binding_count_method_exists(self): - """Test TagService.get_tag_binding_count exists.""" - assert hasattr(TagService, "get_tag_binding_count") - - @patch.object(TagService, "get_tags") - def test_get_tags_returns_list(self, mock_get): - """Test get_tags returns list of tags.""" - mock_tags = [ - Mock(id="tag1", name="Tag One", type="knowledge"), - Mock(id="tag2", name="Tag Two", type="knowledge"), - ] - mock_get.return_value = mock_tags - - result = TagService.get_tags("knowledge", "tenant_123") - assert len(result) == 2 - - @patch.object(TagService, "save_tags") - def test_save_tags_returns_tag(self, mock_save): - """Test save_tags returns created tag.""" - mock_tag = Mock() - mock_tag.id = str(uuid.uuid4()) - mock_tag.name = "New Tag" - mock_tag.type = TagType.KNOWLEDGE - mock_save.return_value = mock_tag - - result = TagService.save_tags({"name": "New Tag", "type": "knowledge"}) - assert result.name == "New Tag" - - -class TestDocumentStatusAction: - """Test document status action values.""" - - def test_enable_action(self): - """Test enable action.""" - action = "enable" - assert action in ["enable", "disable", "archive", "un_archive"] - - def test_disable_action(self): - """Test disable action.""" - action = "disable" - assert action in ["enable", "disable", "archive", "un_archive"] - - def test_archive_action(self): - """Test archive action.""" - action = "archive" - assert action in ["enable", "disable", "archive", "un_archive"] - - def test_un_archive_action(self): - """Test un_archive action.""" - action = "un_archive" - assert action in ["enable", "disable", "archive", "un_archive"] - - -# ============================================================================= -# API Endpoint Tests -# -# ``DatasetListApi`` and ``DatasetApi`` inherit from ``DatasetApiResource`` -# whose ``method_decorators`` include ``validate_dataset_token``. -# -# Decorator strategy: -# - ``@cloud_edition_billing_rate_limit_check`` preserves ``__wrapped__`` -# → call via ``_unwrap(method)(self, …)``. -# - Methods without billing decorators → call directly; only patch ``db``, -# services, ``current_user``, and ``marshal``. -# ============================================================================= +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- def _unwrap(method): @@ -920,6 +235,15 @@ def _unwrap(method): return fn +@pytest.fixture +def app(flask_app_with_containers): + # Uses the full containerised app so that Flask config, extensions, and + # blueprint registrations match production. Most tests mock the service + # layer to isolate controller logic; a few (e.g. test_list_tags_from_db) + # exercise the real DB-backed path to validate end-to-end behaviour. + return flask_app_with_containers + + @pytest.fixture def mock_tenant(): tenant = Mock() @@ -938,12 +262,13 @@ def mock_dataset(): return dataset -class TestDatasetListApiGet: - """Test suite for DatasetListApi.get() endpoint. +# --------------------------------------------------------------------------- +# API endpoint tests — DatasetListApi +# --------------------------------------------------------------------------- - ``get`` has no billing decorators but calls ``current_user``, - ``DatasetService``, ``create_plugin_provider_manager``, and ``marshal``. - """ + +class TestDatasetListApiGet: + """Test suite for DatasetListApi.get() endpoint.""" @patch("controllers.service_api.dataset.dataset.marshal") @patch("controllers.service_api.dataset.dataset.create_plugin_provider_manager") @@ -958,7 +283,6 @@ class TestDatasetListApiGet: app, mock_tenant, ): - """Test successful dataset list retrieval.""" from controllers.service_api.dataset.dataset import DatasetListApi mock_current_user.__class__ = Account @@ -981,10 +305,7 @@ class TestDatasetListApiGet: class TestDatasetListApiPost: - """Test suite for DatasetListApi.post() endpoint. - - ``post`` is wrapped by ``@cloud_edition_billing_rate_limit_check``. - """ + """Test suite for DatasetListApi.post() endpoint.""" @patch("controllers.service_api.dataset.dataset.marshal") @patch("controllers.service_api.dataset.dataset.current_user") @@ -997,7 +318,6 @@ class TestDatasetListApiPost: app, mock_tenant, ): - """Test successful dataset creation.""" from controllers.service_api.dataset.dataset import DatasetListApi mock_current_user.__class__ = Account @@ -1024,7 +344,6 @@ class TestDatasetListApiPost: app, mock_tenant, ): - """Test DatasetNameDuplicateError when name already exists.""" from controllers.service_api.dataset.dataset import DatasetListApi mock_current_user.__class__ = Account @@ -1040,12 +359,13 @@ class TestDatasetListApiPost: _unwrap(api.post)(api, tenant_id=mock_tenant.id) -class TestDatasetApiGet: - """Test suite for DatasetApi.get() endpoint. +# --------------------------------------------------------------------------- +# API endpoint tests — DatasetApi +# --------------------------------------------------------------------------- - ``get`` has no billing decorators but calls ``DatasetService``, - ``create_plugin_provider_manager``, ``marshal``, and ``current_user``. - """ + +class TestDatasetApiGet: + """Test suite for DatasetApi.get() endpoint.""" @patch("controllers.service_api.dataset.dataset.DatasetPermissionService") @patch("controllers.service_api.dataset.dataset.marshal") @@ -1062,7 +382,6 @@ class TestDatasetApiGet: app, mock_dataset, ): - """Test successful dataset retrieval.""" from controllers.service_api.dataset.dataset import DatasetApi mock_dataset_svc.get_dataset.return_value = mock_dataset @@ -1092,7 +411,6 @@ class TestDatasetApiGet: @patch("controllers.service_api.dataset.dataset.DatasetService") def test_get_dataset_not_found(self, mock_dataset_svc, app, mock_dataset): - """Test 404 when dataset not found.""" from controllers.service_api.dataset.dataset import DatasetApi mock_dataset_svc.get_dataset.return_value = None @@ -1114,7 +432,6 @@ class TestDatasetApiGet: app, mock_dataset, ): - """Test 403 when user has no permission.""" from controllers.service_api.dataset.dataset import DatasetApi mock_dataset_svc.get_dataset.return_value = mock_dataset @@ -1130,10 +447,7 @@ class TestDatasetApiGet: class TestDatasetApiDelete: - """Test suite for DatasetApi.delete() endpoint. - - ``delete`` is wrapped by ``@cloud_edition_billing_rate_limit_check``. - """ + """Test suite for DatasetApi.delete() endpoint.""" @patch("controllers.service_api.dataset.dataset.DatasetPermissionService") @patch("controllers.service_api.dataset.dataset.current_user") @@ -1146,7 +460,6 @@ class TestDatasetApiDelete: app, mock_dataset, ): - """Test successful dataset deletion.""" from controllers.service_api.dataset.dataset import DatasetApi mock_dataset_svc.delete_dataset.return_value = True @@ -1169,7 +482,6 @@ class TestDatasetApiDelete: app, mock_dataset, ): - """Test 404 when dataset not found for deletion.""" from controllers.service_api.dataset.dataset import DatasetApi mock_dataset_svc.delete_dataset.return_value = False @@ -1191,7 +503,6 @@ class TestDatasetApiDelete: app, mock_dataset, ): - """Test DatasetInUseError when dataset is in use.""" from controllers.service_api.dataset.dataset import DatasetApi mock_dataset_svc.delete_dataset.side_effect = services.errors.dataset.DatasetInUseError() @@ -1205,12 +516,13 @@ class TestDatasetApiDelete: _unwrap(api.delete)(api, _=mock_dataset.tenant_id, dataset_id=mock_dataset.id) -class TestDocumentStatusApiPatch: - """Test suite for DocumentStatusApi.patch() endpoint. +# --------------------------------------------------------------------------- +# API endpoint tests — DocumentStatusApi +# --------------------------------------------------------------------------- - ``patch`` has no billing decorators but calls ``DatasetService``, - ``DocumentService``, and ``current_user``. - """ + +class TestDocumentStatusApiPatch: + """Test suite for DocumentStatusApi.patch() endpoint.""" @patch("controllers.service_api.dataset.dataset.DocumentService") @patch("controllers.service_api.dataset.dataset.current_user") @@ -1224,7 +536,6 @@ class TestDocumentStatusApiPatch: mock_tenant, mock_dataset, ): - """Test successful batch document status update.""" from controllers.service_api.dataset.dataset import DocumentStatusApi mock_current_user.__class__ = Account @@ -1256,7 +567,6 @@ class TestDocumentStatusApiPatch: mock_tenant, mock_dataset, ): - """Test 404 when dataset not found.""" from controllers.service_api.dataset.dataset import DocumentStatusApi mock_dataset_svc.get_dataset.return_value = None @@ -1274,6 +584,39 @@ class TestDocumentStatusApiPatch: action="enable", ) + @patch("controllers.service_api.dataset.dataset.DocumentService") + @patch("controllers.service_api.dataset.dataset.current_user") + @patch("controllers.service_api.dataset.dataset.DatasetService") + def test_batch_update_status_permission_error( + self, + mock_dataset_svc, + mock_current_user, + mock_doc_svc, + app, + mock_tenant, + mock_dataset, + ): + from controllers.service_api.dataset.dataset import DocumentStatusApi + + mock_current_user.__class__ = Account + mock_dataset_svc.get_dataset.return_value = mock_dataset + mock_dataset_svc.check_dataset_permission.side_effect = services.errors.account.NoPermissionError( + "No permission" + ) + + with app.test_request_context( + f"/datasets/{mock_dataset.id}/documents/status/enable", + method="PATCH", + json={"document_ids": ["doc-1"]}, + ): + api = DocumentStatusApi() + with pytest.raises(Forbidden): + api.patch( + tenant_id=mock_tenant.id, + dataset_id=mock_dataset.id, + action="enable", + ) + @patch("controllers.service_api.dataset.dataset.DocumentService") @patch("controllers.service_api.dataset.dataset.current_user") @patch("controllers.service_api.dataset.dataset.DatasetService") @@ -1286,7 +629,6 @@ class TestDocumentStatusApiPatch: mock_tenant, mock_dataset, ): - """Test InvalidActionError when document is indexing.""" from controllers.service_api.dataset.dataset import DocumentStatusApi mock_current_user.__class__ = Account @@ -1320,7 +662,6 @@ class TestDocumentStatusApiPatch: mock_tenant, mock_dataset, ): - """Test InvalidActionError when ValueError raised.""" from controllers.service_api.dataset.dataset import DocumentStatusApi mock_current_user.__class__ = Account @@ -1343,6 +684,11 @@ class TestDocumentStatusApiPatch: ) +# --------------------------------------------------------------------------- +# API endpoint tests — Tags +# --------------------------------------------------------------------------- + + class TestDatasetTagsApiGet: """Test suite for DatasetTagsApi.get() endpoint.""" @@ -1354,7 +700,6 @@ class TestDatasetTagsApiGet: mock_tag_svc, app, ): - """Test successful tag list retrieval.""" from controllers.service_api.dataset.dataset import DatasetTagsApi mock_current_user.__class__ = Account @@ -1368,15 +713,49 @@ class TestDatasetTagsApiGet: assert status == 200 assert len(response) == 1 + mock_tag_svc.get_tags.assert_called_once_with("knowledge", "tenant-1") + + @pytest.mark.skip(reason="Production bug: DataSetTag.binding_count is str|None but DB COUNT() returns int") + @patch("controllers.service_api.dataset.dataset.current_user") + def test_list_tags_from_db( + self, + mock_current_user, + app, + db_session_with_containers: Session, + ): + """Integration test: creates real Tag rows and retrieves them + through the controller without mocking TagService.""" + from tests.test_containers_integration_tests.controllers.console.helpers import ( + create_console_account_and_tenant, + ) + + account, tenant = create_console_account_and_tenant(db_session_with_containers) + + tag = Tag( + name="Integration Tag", + type=TagType.KNOWLEDGE, + created_by=account.id, + tenant_id=tenant.id, + ) + db_session_with_containers.add(tag) + db_session_with_containers.commit() + + mock_current_user.__class__ = Account + mock_current_user.current_tenant_id = tenant.id + + from controllers.service_api.dataset.dataset import DatasetTagsApi + + with app.test_request_context("/datasets/tags", method="GET"): + api = DatasetTagsApi() + response, status = api.get(_=None) + + assert status == 200 + assert any(t["name"] == "Integration Tag" for t in response) class TestDatasetTagsApiPost: """Test suite for DatasetTagsApi.post() endpoint.""" - # BUG: dataset.py L512 passes ``binding_count=0`` (int) to - # ``DataSetTag.model_validate()``, but ``DataSetTag.binding_count`` - # is typed ``str | None`` (see fields/tag_fields.py L20). - # This causes a Pydantic ValidationError at runtime. @pytest.mark.skip(reason="Production bug: DataSetTag.binding_count is str|None but dataset.py passes int 0") @patch("controllers.service_api.dataset.dataset.TagService") @patch("controllers.service_api.dataset.dataset.current_user") @@ -1386,7 +765,6 @@ class TestDatasetTagsApiPost: mock_tag_svc, app, ): - """Test successful tag creation.""" from controllers.service_api.dataset.dataset import DatasetTagsApi mock_current_user.__class__ = Account @@ -1409,7 +787,6 @@ class TestDatasetTagsApiPost: @patch("controllers.service_api.dataset.dataset.current_user") def test_create_tag_forbidden(self, mock_current_user, app): - """Test 403 when user lacks edit permission.""" from controllers.service_api.dataset.dataset import DatasetTagsApi mock_current_user.__class__ = Account @@ -1426,6 +803,146 @@ class TestDatasetTagsApiPost: api.post(_=None) +class TestDatasetTagsApiPatch: + """Test suite for DatasetTagsApi.patch() endpoint.""" + + @pytest.mark.skip(reason="Production bug: DataSetTag.binding_count is str|None but dataset.py passes int 0") + @patch("controllers.service_api.dataset.dataset.TagService") + @patch("controllers.service_api.dataset.dataset.service_api_ns") + @patch("controllers.service_api.dataset.dataset.current_user") + def test_update_tag_success( + self, + mock_current_user, + mock_service_api_ns, + mock_tag_svc, + app, + ): + from controllers.service_api.dataset.dataset import DatasetTagsApi + + mock_current_user.__class__ = Account + mock_current_user.has_edit_permission = True + mock_current_user.is_dataset_editor = True + + mock_tag = SimpleNamespace(id="tag-1", name="Updated Tag", type="knowledge") + mock_tag_svc.update_tags.return_value = mock_tag + mock_tag_svc.get_tag_binding_count.return_value = 5 + mock_service_api_ns.payload = {"name": "Updated Tag", "tag_id": "tag-1"} + + with app.test_request_context( + "/datasets/tags", + method="PATCH", + json={"name": "Updated Tag", "tag_id": "tag-1"}, + ): + api = DatasetTagsApi() + response, status = api.patch(_=None) + + assert status == 200 + assert response["name"] == "Updated Tag" + mock_tag_svc.update_tags.assert_called_once_with({"name": "Updated Tag", "type": "knowledge"}, "tag-1") + + @patch("controllers.service_api.dataset.dataset.current_user") + def test_update_tag_forbidden(self, mock_current_user, app): + from controllers.service_api.dataset.dataset import DatasetTagsApi + + mock_current_user.__class__ = Account + mock_current_user.has_edit_permission = False + mock_current_user.is_dataset_editor = False + + with app.test_request_context( + "/datasets/tags", + method="PATCH", + json={"name": "Updated Tag", "tag_id": "tag-1"}, + ): + api = DatasetTagsApi() + with pytest.raises(Forbidden): + api.patch(_=None) + + +class TestDatasetTagsApiDelete: + """Test suite for DatasetTagsApi.delete() endpoint.""" + + @patch("controllers.service_api.dataset.dataset.TagService") + @patch("controllers.service_api.dataset.dataset.service_api_ns") + @patch("libs.login.current_user") + def test_delete_tag_success( + self, + mock_current_user, + mock_service_api_ns, + mock_tag_svc, + app, + ): + from controllers.service_api.dataset.dataset import DatasetTagsApi + + user_obj = Mock(spec=Account) + user_obj.has_edit_permission = True + mock_current_user.has_edit_permission = True + # Assign as plain lambda to avoid AsyncMock returning a coroutine + mock_current_user._get_current_object = lambda: user_obj + + mock_tag_svc.delete_tag.return_value = None + mock_service_api_ns.payload = {"tag_id": "tag-1"} + + with app.test_request_context( + "/datasets/tags", + method="DELETE", + json={"tag_id": "tag-1"}, + ): + api = DatasetTagsApi() + result = api.delete(_=None) + + assert result == ("", 204) + mock_tag_svc.delete_tag.assert_called_once_with("tag-1") + + @patch("libs.login.current_user") + def test_delete_tag_forbidden(self, mock_current_user, app): + from controllers.service_api.dataset.dataset import DatasetTagsApi + + user_obj = Mock(spec=Account) + user_obj.has_edit_permission = False + mock_current_user.has_edit_permission = False + # Assign as plain lambda to avoid AsyncMock returning a coroutine + mock_current_user._get_current_object = lambda: user_obj + + with app.test_request_context( + "/datasets/tags", + method="DELETE", + json={"tag_id": "tag-1"}, + ): + api = DatasetTagsApi() + with pytest.raises(Forbidden): + api.delete(_=None) + + +class TestDatasetTagsBindingStatusApi: + """Test suite for DatasetTagsBindingStatusApi endpoints.""" + + @patch("controllers.service_api.dataset.dataset.TagService") + @patch("controllers.service_api.dataset.dataset.current_user") + def test_get_dataset_tags_binding_status( + self, + mock_current_user, + mock_tag_svc, + app, + ): + from controllers.service_api.dataset.dataset import DatasetTagsBindingStatusApi + + mock_current_user.__class__ = Account + mock_current_user.current_tenant_id = "tenant_123" + mock_tag = Mock() + mock_tag.id = "tag_1" + mock_tag.name = "Test Tag" + mock_tag_svc.get_tags_by_target_id.return_value = [mock_tag] + + with app.test_request_context("/", method="GET"): + api = DatasetTagsBindingStatusApi() + response, status_code = api.get("tenant_123", dataset_id="dataset_123") + + assert status_code == 200 + assert response["data"] == [{"id": "tag_1", "name": "Test Tag"}] + assert response["total"] == 1 + mock_tag_svc.get_tags_by_target_id.assert_called_once_with("knowledge", "tenant_123", "dataset_123") + + class TestDatasetTagBindingApiPost: """Test suite for DatasetTagBindingApi.post() endpoint.""" @@ -1437,7 +954,6 @@ class TestDatasetTagBindingApiPost: mock_tag_svc, app, ): - """Test successful tag binding.""" from controllers.service_api.dataset.dataset import DatasetTagBindingApi mock_current_user.__class__ = Account @@ -1454,10 +970,14 @@ class TestDatasetTagBindingApiPost: result = api.post(_=None) assert result == ("", 204) + from services.tag_service import TagBindingCreatePayload + + mock_tag_svc.save_tag_binding.assert_called_once_with( + TagBindingCreatePayload(tag_ids=["tag-1"], target_id="ds-1", type="knowledge") + ) @patch("controllers.service_api.dataset.dataset.current_user") def test_bind_tags_forbidden(self, mock_current_user, app): - """Test 403 when user lacks edit permission.""" from controllers.service_api.dataset.dataset import DatasetTagBindingApi mock_current_user.__class__ = Account @@ -1485,7 +1005,6 @@ class TestDatasetTagUnbindingApiPost: mock_tag_svc, app, ): - """Test successful tag unbinding.""" from controllers.service_api.dataset.dataset import DatasetTagUnbindingApi mock_current_user.__class__ = Account @@ -1502,10 +1021,14 @@ class TestDatasetTagUnbindingApiPost: result = api.post(_=None) assert result == ("", 204) + from services.tag_service import TagBindingDeletePayload + + mock_tag_svc.delete_tag_binding.assert_called_once_with( + TagBindingDeletePayload(tag_id="tag-1", target_id="ds-1", type="knowledge") + ) @patch("controllers.service_api.dataset.dataset.current_user") def test_unbind_tag_forbidden(self, mock_current_user, app): - """Test 403 when user lacks edit permission.""" from controllers.service_api.dataset.dataset import DatasetTagUnbindingApi mock_current_user.__class__ = Account diff --git a/api/tests/test_containers_integration_tests/models/test_types_enum_text.py b/api/tests/test_containers_integration_tests/models/test_types_enum_text.py index 9cf96c1ca7..8aec6b6acc 100644 --- a/api/tests/test_containers_integration_tests/models/test_types_enum_text.py +++ b/api/tests/test_containers_integration_tests/models/test_types_enum_text.py @@ -4,6 +4,7 @@ from typing import Any, NamedTuple import pytest import sqlalchemy as sa +from graphon.model_runtime.entities.model_entities import ModelType from sqlalchemy import exc as sa_exc from sqlalchemy import insert from sqlalchemy.engine import Connection, Engine @@ -58,6 +59,13 @@ class _ColumnTest(_Base): long_value: Mapped[_EnumWithLongValue] = mapped_column(EnumText(enum_class=_EnumWithLongValue), nullable=False) +class _LegacyModelTypeRecord(_Base): + __tablename__ = "enum_text_legacy_model_type_test" + + id: Mapped[int] = mapped_column(sa.Integer, primary_key=True) + model_type: Mapped[ModelType] = mapped_column(EnumText(enum_class=ModelType), nullable=False) + + def _first[T](it: Iterable[T]) -> T: ls = list(it) if not ls: @@ -201,3 +209,23 @@ class TestEnumText: _user = session.query(_User).where(_User.id == 1).first() assert str(exc.value) == "'invalid' is not a valid _UserType" + + def test_select_legacy_model_type_values(self, engine_with_containers: Engine): + insertion_sql = """ + INSERT INTO enum_text_legacy_model_type_test (id, model_type) VALUES + (1, 'text-generation'), + (2, 'embeddings'), + (3, 'reranking'); + """ + with Session(engine_with_containers) as session: + session.execute(sa.text(insertion_sql)) + session.commit() + + with Session(engine_with_containers) as session: + records = session.query(_LegacyModelTypeRecord).order_by(_LegacyModelTypeRecord.id).all() + + assert [record.model_type for record in records] == [ + ModelType.LLM, + ModelType.TEXT_EMBEDDING, + ModelType.RERANK, + ] diff --git a/api/tests/test_containers_integration_tests/services/test_tag_service.py b/api/tests/test_containers_integration_tests/services/test_tag_service.py index f504f35589..5a6bf0466e 100644 --- a/api/tests/test_containers_integration_tests/services/test_tag_service.py +++ b/api/tests/test_containers_integration_tests/services/test_tag_service.py @@ -12,7 +12,13 @@ from models import Account, Tenant, TenantAccountJoin, TenantAccountRole from models.dataset import Dataset from models.enums import DataSourceType, TagType from models.model import App, Tag, TagBinding -from services.tag_service import TagService +from services.tag_service import ( + SaveTagPayload, + TagBindingCreatePayload, + TagBindingDeletePayload, + TagService, + UpdateTagPayload, +) class TestTagService: @@ -685,7 +691,7 @@ class TestTagService: db_session_with_containers, mock_external_service_dependencies ) - tag_args = {"name": "test_tag_name", "type": "knowledge"} + tag_args = SaveTagPayload(name="test_tag_name", type="knowledge") # Act: Execute the method under test result = TagService.save_tags(tag_args) @@ -725,7 +731,7 @@ class TestTagService: ) # Create first tag - tag_args = {"name": "duplicate_tag", "type": "app"} + tag_args = SaveTagPayload(name="duplicate_tag", type="app") TagService.save_tags(tag_args) # Act & Assert: Verify proper error handling @@ -749,11 +755,11 @@ class TestTagService: ) # Create a tag to update - tag_args = {"name": "original_name", "type": "knowledge"} + tag_args = SaveTagPayload(name="original_name", type="knowledge") tag = TagService.save_tags(tag_args) # Update args - update_args = {"name": "updated_name", "type": "knowledge"} + update_args = UpdateTagPayload(name="updated_name", type="knowledge") # Act: Execute the method under test result = TagService.update_tags(update_args, tag.id) @@ -793,7 +799,7 @@ class TestTagService: non_existent_tag_id = str(uuid.uuid4()) - update_args = {"name": "updated_name", "type": "knowledge"} + update_args = UpdateTagPayload(name="updated_name", type="knowledge") # Act & Assert: Verify proper error handling with pytest.raises(NotFound) as exc_info: @@ -817,14 +823,14 @@ class TestTagService: ) # Create two tags - tag1_args = {"name": "first_tag", "type": "app"} + tag1_args = SaveTagPayload(name="first_tag", type="app") tag1 = TagService.save_tags(tag1_args) - tag2_args = {"name": "second_tag", "type": "app"} + tag2_args = SaveTagPayload(name="second_tag", type="app") tag2 = TagService.save_tags(tag2_args) # Try to update second tag with first tag's name - update_args = {"name": "first_tag", "type": "app"} + update_args = UpdateTagPayload(name="first_tag", type="app") # Act & Assert: Verify proper error handling with pytest.raises(ValueError) as exc_info: @@ -988,8 +994,10 @@ class TestTagService: dataset = self._create_test_dataset(db_session_with_containers, mock_external_service_dependencies, tenant.id) # Act: Execute the method under test - binding_args = {"type": "knowledge", "target_id": dataset.id, "tag_ids": [tag.id for tag in tags]} - TagService.save_tag_binding(binding_args) + binding_payload = TagBindingCreatePayload( + type="knowledge", target_id=dataset.id, tag_ids=[tag.id for tag in tags] + ) + TagService.save_tag_binding(binding_payload) # Assert: Verify the expected outcomes @@ -1030,11 +1038,11 @@ class TestTagService: app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, tenant.id) # Create first binding - binding_args = {"type": "app", "target_id": app.id, "tag_ids": [tag.id]} - TagService.save_tag_binding(binding_args) + binding_payload = TagBindingCreatePayload(type="app", target_id=app.id, tag_ids=[tag.id]) + TagService.save_tag_binding(binding_payload) # Act: Try to create duplicate binding - TagService.save_tag_binding(binding_args) + TagService.save_tag_binding(binding_payload) # Assert: Verify the expected outcomes @@ -1071,11 +1079,10 @@ class TestTagService: non_existent_target_id = str(uuid.uuid4()) # Act & Assert: Verify proper error handling - binding_args = {"type": "invalid_type", "target_id": non_existent_target_id, "tag_ids": [tag.id]} + from pydantic import ValidationError - with pytest.raises(NotFound) as exc_info: - TagService.save_tag_binding(binding_args) - assert "Invalid binding type" in str(exc_info.value) + with pytest.raises(ValidationError): + TagBindingCreatePayload(type="invalid_type", target_id=non_existent_target_id, tag_ids=[tag.id]) def test_delete_tag_binding_success(self, db_session_with_containers: Session, mock_external_service_dependencies): """ @@ -1113,8 +1120,8 @@ class TestTagService: assert binding_before is not None # Act: Execute the method under test - delete_args = {"type": "knowledge", "target_id": dataset.id, "tag_id": tag.id} - TagService.delete_tag_binding(delete_args) + delete_payload = TagBindingDeletePayload(type="knowledge", target_id=dataset.id, tag_id=tag.id) + TagService.delete_tag_binding(delete_payload) # Assert: Verify the expected outcomes # Verify tag binding was deleted @@ -1149,8 +1156,8 @@ class TestTagService: app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, tenant.id) # Act: Try to delete non-existent binding - delete_args = {"type": "app", "target_id": app.id, "tag_id": tag.id} - TagService.delete_tag_binding(delete_args) + delete_payload = TagBindingDeletePayload(type="app", target_id=app.id, tag_id=tag.id) + TagService.delete_tag_binding(delete_payload) # Assert: Verify the expected outcomes # No error should be raised, and database state should remain unchanged diff --git a/api/tests/unit_tests/controllers/console/test_workspace_account.py b/api/tests/unit_tests/controllers/console/test_workspace_account.py index 9afc1c4166..7f9fe9cbf9 100644 --- a/api/tests/unit_tests/controllers/console/test_workspace_account.py +++ b/api/tests/unit_tests/controllers/console/test_workspace_account.py @@ -20,7 +20,7 @@ def app(): app = Flask(__name__) app.config["TESTING"] = True app.config["RESTX_MASK_HEADER"] = "X-Fields" - app.login_manager = SimpleNamespace(_load_user=lambda: None) + app.login_manager = SimpleNamespace(load_user_from_request_context=lambda: None) return app diff --git a/api/tests/unit_tests/controllers/console/test_workspace_members.py b/api/tests/unit_tests/controllers/console/test_workspace_members.py index 368892b922..239fec8430 100644 --- a/api/tests/unit_tests/controllers/console/test_workspace_members.py +++ b/api/tests/unit_tests/controllers/console/test_workspace_members.py @@ -12,7 +12,7 @@ from models.account import Account, TenantAccountRole def app(): flask_app = Flask(__name__) flask_app.config["TESTING"] = True - flask_app.login_manager = SimpleNamespace(_load_user=lambda: None) + flask_app.login_manager = SimpleNamespace(load_user_from_request_context=lambda: None) return flask_app diff --git a/api/tests/unit_tests/controllers/inner_api/app/test_dsl.py b/api/tests/unit_tests/controllers/inner_api/app/test_dsl.py index 4a5f91cc5d..974d8f7bc6 100644 --- a/api/tests/unit_tests/controllers/inner_api/app/test_dsl.py +++ b/api/tests/unit_tests/controllers/inner_api/app/test_dsl.py @@ -102,14 +102,16 @@ class TestEnterpriseAppDSLImport: @pytest.fixture def _mock_import_deps(self): - """Patch db, Session, and AppDslService for import handler tests.""" + """Patch db, sessionmaker, and AppDslService for import handler tests.""" + mock_session_ctx = MagicMock() + mock_session_ctx.__enter__ = MagicMock(return_value=MagicMock()) + mock_session_ctx.__exit__ = MagicMock(return_value=False) + mock_sessionmaker = MagicMock(return_value=MagicMock(begin=MagicMock(return_value=mock_session_ctx))) with ( patch("controllers.inner_api.app.dsl.db"), - patch("controllers.inner_api.app.dsl.Session") as mock_session, + patch("controllers.inner_api.app.dsl.sessionmaker", mock_sessionmaker), patch("controllers.inner_api.app.dsl.AppDslService") as mock_dsl_cls, ): - mock_session.return_value.__enter__ = MagicMock(return_value=MagicMock()) - mock_session.return_value.__exit__ = MagicMock(return_value=False) self._mock_dsl = MagicMock() mock_dsl_cls.return_value = self._mock_dsl yield diff --git a/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline_core.py b/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline_core.py index f7e7b7e20e..f22602a400 100644 --- a/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline_core.py +++ b/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline_core.py @@ -505,13 +505,7 @@ class TestEasyUiBasedGenerateTaskPipeline: def __exit__(self, exc_type, exc, tb): return False - def query(self, *args, **kwargs): - return self - - def where(self, *args, **kwargs): - return self - - def first(self): + def scalar(self, *args, **kwargs): return agent_thought monkeypatch.setattr( @@ -1182,13 +1176,7 @@ class TestEasyUiBasedGenerateTaskPipeline: def __exit__(self, exc_type, exc, tb): return False - def query(self, *args, **kwargs): - return self - - def where(self, *args, **kwargs): - return self - - def first(self): + def scalar(self, *args, **kwargs): return None monkeypatch.setattr("core.app.task_pipeline.easy_ui_based_generate_task_pipeline.Session", _Session) diff --git a/api/tests/unit_tests/core/app/task_pipeline/test_message_cycle_manager_optimization.py b/api/tests/unit_tests/core/app/task_pipeline/test_message_cycle_manager_optimization.py index 07ee75ed35..92fe3cbec6 100644 --- a/api/tests/unit_tests/core/app/task_pipeline/test_message_cycle_manager_optimization.py +++ b/api/tests/unit_tests/core/app/task_pipeline/test_message_cycle_manager_optimization.py @@ -9,7 +9,7 @@ from flask import Flask, current_app from core.app.entities.queue_entities import QueueAnnotationReplyEvent, QueueRetrieverResourcesEvent from core.app.entities.task_entities import MessageStreamResponse, StreamEvent, TaskStateMetadata from core.app.task_pipeline.message_cycle_manager import MessageCycleManager -from core.rag.entities.citation_metadata import RetrievalSourceMetadata +from core.rag.entities import RetrievalSourceMetadata from models.model import AppMode diff --git a/api/tests/unit_tests/core/datasource/test_datasource_manager.py b/api/tests/unit_tests/core/datasource/test_datasource_manager.py index b0c72ee42f..d338cadb77 100644 --- a/api/tests/unit_tests/core/datasource/test_datasource_manager.py +++ b/api/tests/unit_tests/core/datasource/test_datasource_manager.py @@ -632,16 +632,6 @@ def test_get_upload_file_by_id_builds_file(mocker): source_url="http://x", ) - class _Q: - def __init__(self, row): - self._row = row - - def where(self, *_args, **_kwargs): - return self - - def first(self): - return self._row - class _S: def __init__(self, row): self._row = row @@ -652,8 +642,8 @@ def test_get_upload_file_by_id_builds_file(mocker): def __exit__(self, *exc): return False - def query(self, *_): - return _Q(self._row) + def scalar(self, *_args, **_kwargs): + return self._row mocker.patch("core.datasource.datasource_manager.session_factory.create_session", return_value=_S(fake_row)) @@ -665,13 +655,6 @@ def test_get_upload_file_by_id_builds_file(mocker): def test_get_upload_file_by_id_raises_when_missing(mocker): - class _Q: - def where(self, *_args, **_kwargs): - return self - - def first(self): - return None - class _S: def __enter__(self): return self @@ -679,8 +662,8 @@ def test_get_upload_file_by_id_raises_when_missing(mocker): def __exit__(self, *exc): return False - def query(self, *_): - return _Q() + def scalar(self, *_args, **_kwargs): + return None mocker.patch("core.datasource.datasource_manager.session_factory.create_session", return_value=_S()) diff --git a/api/tests/unit_tests/core/helper/test_credential_utils.py b/api/tests/unit_tests/core/helper/test_credential_utils.py index 7e0d7d0af7..dd10f81b02 100644 --- a/api/tests/unit_tests/core/helper/test_credential_utils.py +++ b/api/tests/unit_tests/core/helper/test_credential_utils.py @@ -4,8 +4,8 @@ from typing import cast import pytest from pytest_mock import MockerFixture +from core.entities import PluginCredentialType from core.helper.credential_utils import check_credential_policy_compliance, is_credential_exists -from services.enterprise.plugin_manager_service import PluginCredentialType def test_check_credential_policy_compliance_returns_when_feature_disabled( diff --git a/api/tests/unit_tests/core/llm_generator/test_llm_generator.py b/api/tests/unit_tests/core/llm_generator/test_llm_generator.py index 62e714deb6..7cdfb31189 100644 --- a/api/tests/unit_tests/core/llm_generator/test_llm_generator.py +++ b/api/tests/unit_tests/core/llm_generator/test_llm_generator.py @@ -346,13 +346,13 @@ class TestLLMGenerator: def test_instruction_modify_workflow_app_not_found(self): with patch("extensions.ext_database.db.session") as mock_session: - mock_session.return_value.query.return_value.where.return_value.first.return_value = None + mock_session.return_value.scalar.return_value = None with pytest.raises(ValueError, match="App not found."): LLMGenerator.instruction_modify_workflow("t", "f", "n", "c", "i", MagicMock(), "o", MagicMock()) def test_instruction_modify_workflow_no_workflow(self): with patch("extensions.ext_database.db.session") as mock_session: - mock_session.return_value.query.return_value.where.return_value.first.return_value = MagicMock() + mock_session.return_value.scalar.return_value = MagicMock() workflow_service = MagicMock() workflow_service.get_draft_workflow.return_value = None with pytest.raises(ValueError, match="Workflow not found for the given app model."): @@ -360,7 +360,7 @@ class TestLLMGenerator: def test_instruction_modify_workflow_success(self, mock_model_instance, model_config_entity): with patch("extensions.ext_database.db.session") as mock_session: - mock_session.return_value.query.return_value.where.return_value.first.return_value = MagicMock() + mock_session.return_value.scalar.return_value = MagicMock() workflow = MagicMock() workflow.graph_dict = {"graph": {"nodes": [{"id": "node_id", "data": {"type": "llm"}}]}} diff --git a/api/tests/unit_tests/core/ops/tencent_trace/test_tencent_trace.py b/api/tests/unit_tests/core/ops/tencent_trace/test_tencent_trace.py index 382e5dadc3..f67abba807 100644 --- a/api/tests/unit_tests/core/ops/tencent_trace/test_tencent_trace.py +++ b/api/tests/unit_tests/core/ops/tencent_trace/test_tencent_trace.py @@ -407,8 +407,7 @@ class TestTencentDataTrace: mock_db.engine = "engine" with patch("core.ops.tencent_trace.tencent_trace.Session") as mock_session_ctx: session = mock_session_ctx.return_value.__enter__.return_value - session.scalar.side_effect = [app, account] - session.query.return_value.filter_by.return_value.first.return_value = tenant_join + session.scalar.side_effect = [app, account, tenant_join] with patch( "core.ops.tencent_trace.tencent_trace.SQLAlchemyWorkflowNodeExecutionRepository" diff --git a/api/tests/unit_tests/core/ops/test_base_trace_instance.py b/api/tests/unit_tests/core/ops/test_base_trace_instance.py index a8bee7dfa7..ac65d13454 100644 --- a/api/tests/unit_tests/core/ops/test_base_trace_instance.py +++ b/api/tests/unit_tests/core/ops/test_base_trace_instance.py @@ -76,10 +76,7 @@ def test_get_service_account_with_tenant_tenant_not_found(mock_db_session): mock_account = MagicMock(spec=Account) mock_account.id = "creator_id" - mock_db_session.scalar.side_effect = [mock_app, mock_account] - - # session.query(TenantAccountJoin).filter_by(...).first() returns None - mock_db_session.query.return_value.filter_by.return_value.first.return_value = None + mock_db_session.scalar.side_effect = [mock_app, mock_account, None] config = MagicMock(spec=BaseTracingConfig) instance = ConcreteTraceInstance(config) @@ -97,11 +94,10 @@ def test_get_service_account_with_tenant_success(mock_db_session): mock_account.id = "creator_id" mock_account.set_tenant_id = MagicMock() - mock_db_session.scalar.side_effect = [mock_app, mock_account] - mock_tenant_join = MagicMock(spec=TenantAccountJoin) mock_tenant_join.tenant_id = "tenant_id" - mock_db_session.query.return_value.filter_by.return_value.first.return_value = mock_tenant_join + + mock_db_session.scalar.side_effect = [mock_app, mock_account, mock_tenant_join] config = MagicMock(spec=BaseTracingConfig) instance = ConcreteTraceInstance(config) diff --git a/api/tests/unit_tests/core/rag/datasource/test_datasource_retrieval.py b/api/tests/unit_tests/core/rag/datasource/test_datasource_retrieval.py index 5dbd62580a..8b104597a8 100644 --- a/api/tests/unit_tests/core/rag/datasource/test_datasource_retrieval.py +++ b/api/tests/unit_tests/core/rag/datasource/test_datasource_retrieval.py @@ -119,6 +119,14 @@ class _FakeSummaryQuery: return self._summaries +class _FakeScalarsResult: + def __init__(self, data: list) -> None: + self._data = data + + def all(self) -> list: + return self._data + + class _FakeSession: def __init__(self, execute_payloads: list[list], summaries: list) -> None: self._payloads = list(execute_payloads) @@ -128,8 +136,8 @@ class _FakeSession: data = self._payloads.pop(0) if self._payloads else [] return _FakeExecuteResult(data) - def query(self, model): - return _FakeSummaryQuery(self._summaries) + def scalars(self, stmt): + return _FakeScalarsResult(self._summaries) class _FakeSessionContext: @@ -228,7 +236,7 @@ class TestRetrievalServiceInternals: assert mock_retrieve.call_count == 2 @patch("core.rag.datasource.retrieval_service.ExternalDatasetService.fetch_external_knowledge_retrieval") - @patch("core.rag.datasource.retrieval_service.MetadataCondition.model_validate") + @patch("core.rag.datasource.retrieval_service.MetadataFilteringCondition.model_validate") @patch("core.rag.datasource.retrieval_service.db.session.scalar") def test_external_retrieve_with_metadata_conditions(self, mock_scalar, mock_validate, mock_fetch): mock_scalar.return_value = SimpleNamespace(tenant_id="tenant-1") @@ -265,14 +273,14 @@ class TestRetrievalServiceInternals: def test_get_dataset_queries_by_id(self, mock_session_class): expected_dataset = Mock(spec=Dataset) mock_session = Mock() - mock_session.query.return_value.where.return_value.first.return_value = expected_dataset + mock_session.scalar.return_value = expected_dataset mock_session_class.return_value.__enter__.return_value = mock_session with patch.object(retrieval_service_module, "db", SimpleNamespace(engine=Mock())): result = RetrievalService._get_dataset("dataset-123") assert result == expected_dataset - mock_session.query.assert_called_once() + mock_session.scalar.assert_called_once() @patch("core.rag.datasource.retrieval_service.Keyword") @patch("core.rag.datasource.retrieval_service.RetrievalService._get_dataset") @@ -1046,12 +1054,8 @@ class TestRetrievalServiceInternals: size=42, ) binding = SimpleNamespace(segment_id="segment-1", attachment_id="upload-1") - upload_query = Mock() - upload_query.where.return_value.first.return_value = upload_file - binding_query = Mock() - binding_query.where.return_value.first.return_value = binding session = Mock() - session.query.side_effect = [upload_query, binding_query] + session.scalar.side_effect = [upload_file, binding] result = RetrievalService.get_segment_attachment_info("dataset-id", "tenant-id", "upload-1", session) @@ -1076,32 +1080,26 @@ class TestRetrievalServiceInternals: mime_type="image/png", size=42, ) - upload_query = Mock() - upload_query.where.return_value.first.return_value = upload_file - binding_query = Mock() - binding_query.where.return_value.first.return_value = None session = Mock() - session.query.side_effect = [upload_query, binding_query] + session.scalar.side_effect = [upload_file, None] result = RetrievalService.get_segment_attachment_info("dataset-id", "tenant-id", "upload-1", session) assert result is None def test_get_segment_attachment_info_returns_none_when_upload_file_missing(self): - upload_query = Mock() - upload_query.where.return_value.first.return_value = None session = Mock() - session.query.return_value = upload_query + session.scalar.return_value = None result = RetrievalService.get_segment_attachment_info("dataset-id", "tenant-id", "upload-1", session) assert result is None def test_get_segment_attachment_infos_returns_empty_when_upload_files_missing(self): - upload_query = Mock() - upload_query.where.return_value.all.return_value = [] + scalars_result = Mock() + scalars_result.all.return_value = [] session = Mock() - session.query.return_value = upload_query + session.scalars.return_value = scalars_result result = RetrievalService.get_segment_attachment_infos(["upload-1"], session) @@ -1115,12 +1113,12 @@ class TestRetrievalServiceInternals: mime_type="image/png", size=42, ) - upload_query = Mock() - upload_query.where.return_value.all.return_value = [upload_file] - binding_query = Mock() - binding_query.where.return_value.all.return_value = [] + upload_scalars = Mock() + upload_scalars.all.return_value = [upload_file] + binding_scalars = Mock() + binding_scalars.all.return_value = [] session = Mock() - session.query.side_effect = [upload_query, binding_query] + session.scalars.side_effect = [upload_scalars, binding_scalars] result = RetrievalService.get_segment_attachment_infos(["upload-1"], session) @@ -1144,12 +1142,12 @@ class TestRetrievalServiceInternals: ) binding = SimpleNamespace(attachment_id="upload-1", segment_id="segment-1") - upload_query = Mock() - upload_query.where.return_value.all.return_value = [upload_file_1, upload_file_2] - binding_query = Mock() - binding_query.where.return_value.all.return_value = [binding] + upload_scalars = Mock() + upload_scalars.all.return_value = [upload_file_1, upload_file_2] + binding_scalars = Mock() + binding_scalars.all.return_value = [binding] session = Mock() - session.query.side_effect = [upload_query, binding_query] + session.scalars.side_effect = [upload_scalars, binding_scalars] result = RetrievalService.get_segment_attachment_infos(["upload-1", "upload-2"], session) diff --git a/api/tests/unit_tests/core/rag/indexing/processor/test_parent_child_index_processor.py b/api/tests/unit_tests/core/rag/indexing/processor/test_parent_child_index_processor.py index d363a0804d..c241b44d52 100644 --- a/api/tests/unit_tests/core/rag/indexing/processor/test_parent_child_index_processor.py +++ b/api/tests/unit_tests/core/rag/indexing/processor/test_parent_child_index_processor.py @@ -4,10 +4,10 @@ from unittest.mock import MagicMock, Mock, patch import pytest from core.entities.knowledge_entities import PreviewDetail +from core.rag.entities import ParentMode from core.rag.index_processor.constant.index_type import IndexTechniqueType from core.rag.index_processor.processor.parent_child_index_processor import ParentChildIndexProcessor from core.rag.models.document import AttachmentDocument, ChildDocument, Document -from services.entities.knowledge_entities.knowledge_entities import ParentMode class TestParentChildIndexProcessor: diff --git a/api/tests/unit_tests/core/rag/retrieval/test_dataset_retrieval.py b/api/tests/unit_tests/core/rag/retrieval/test_dataset_retrieval.py index fee7b168ad..40d138df90 100644 --- a/api/tests/unit_tests/core/rag/retrieval/test_dataset_retrieval.py +++ b/api/tests/unit_tests/core/rag/retrieval/test_dataset_retrieval.py @@ -10,9 +10,6 @@ from graphon.model_runtime.entities.llm_entities import LLMUsage from graphon.model_runtime.entities.model_entities import ModelFeature from sqlalchemy import column -from core.app.app_config.entities import ( - Condition as AppCondition, -) from core.app.app_config.entities import ( DatasetEntity, DatasetRetrieveConfigEntity, @@ -29,6 +26,7 @@ from core.entities.agent_entities import PlanningStrategy from core.entities.model_entities import ModelStatus from core.rag.data_post_processor.data_post_processor import WeightsDict from core.rag.datasource.retrieval_service import RetrievalService +from core.rag.entities import Condition as AppCondition from core.rag.index_processor.constant.doc_type import DocType from core.rag.index_processor.constant.index_type import IndexStructureType from core.rag.models.document import Document diff --git a/api/tests/unit_tests/extensions/test_celery_ssl.py b/api/tests/unit_tests/extensions/test_celery_ssl.py index 2ec7d6b4fc..81687ce5f8 100644 --- a/api/tests/unit_tests/extensions/test_celery_ssl.py +++ b/api/tests/unit_tests/extensions/test_celery_ssl.py @@ -14,9 +14,9 @@ class TestCelerySSLConfiguration: dify_config = DifyConfig(CELERY_BROKER_URL="redis://localhost:6379/0") with patch("extensions.ext_celery.dify_config", dify_config): - from extensions.ext_celery import _get_celery_ssl_options + from extensions.ext_celery import get_celery_ssl_options - result = _get_celery_ssl_options() + result = get_celery_ssl_options() assert result is None def test_get_celery_ssl_options_when_broker_not_redis(self): @@ -25,9 +25,9 @@ class TestCelerySSLConfiguration: mock_config.CELERY_BROKER_URL = "amqp://localhost:5672" with patch("extensions.ext_celery.dify_config", mock_config): - from extensions.ext_celery import _get_celery_ssl_options + from extensions.ext_celery import get_celery_ssl_options - result = _get_celery_ssl_options() + result = get_celery_ssl_options() assert result is None def test_get_celery_ssl_options_with_cert_none(self): @@ -40,9 +40,9 @@ class TestCelerySSLConfiguration: mock_config.REDIS_SSL_KEYFILE = None with patch("extensions.ext_celery.dify_config", mock_config): - from extensions.ext_celery import _get_celery_ssl_options + from extensions.ext_celery import get_celery_ssl_options - result = _get_celery_ssl_options() + result = get_celery_ssl_options() assert result is not None assert result["ssl_cert_reqs"] == ssl.CERT_NONE assert result["ssl_ca_certs"] is None @@ -59,9 +59,9 @@ class TestCelerySSLConfiguration: mock_config.REDIS_SSL_KEYFILE = "/path/to/client.key" with patch("extensions.ext_celery.dify_config", mock_config): - from extensions.ext_celery import _get_celery_ssl_options + from extensions.ext_celery import get_celery_ssl_options - result = _get_celery_ssl_options() + result = get_celery_ssl_options() assert result is not None assert result["ssl_cert_reqs"] == ssl.CERT_REQUIRED assert result["ssl_ca_certs"] == "/path/to/ca.crt" @@ -78,9 +78,9 @@ class TestCelerySSLConfiguration: mock_config.REDIS_SSL_KEYFILE = None with patch("extensions.ext_celery.dify_config", mock_config): - from extensions.ext_celery import _get_celery_ssl_options + from extensions.ext_celery import get_celery_ssl_options - result = _get_celery_ssl_options() + result = get_celery_ssl_options() assert result is not None assert result["ssl_cert_reqs"] == ssl.CERT_OPTIONAL assert result["ssl_ca_certs"] == "/path/to/ca.crt" @@ -95,9 +95,9 @@ class TestCelerySSLConfiguration: mock_config.REDIS_SSL_KEYFILE = None with patch("extensions.ext_celery.dify_config", mock_config): - from extensions.ext_celery import _get_celery_ssl_options + from extensions.ext_celery import get_celery_ssl_options - result = _get_celery_ssl_options() + result = get_celery_ssl_options() assert result is not None assert result["ssl_cert_reqs"] == ssl.CERT_NONE # Should default to CERT_NONE diff --git a/api/tests/unit_tests/extensions/test_ext_login.py b/api/tests/unit_tests/extensions/test_ext_login.py new file mode 100644 index 0000000000..64abc19427 --- /dev/null +++ b/api/tests/unit_tests/extensions/test_ext_login.py @@ -0,0 +1,17 @@ +import json + +from flask import Response + +from extensions.ext_login import unauthorized_handler + + +def test_unauthorized_handler_returns_json_response() -> None: + response = unauthorized_handler() + + assert isinstance(response, Response) + assert response.status_code == 401 + assert response.content_type == "application/json" + assert json.loads(response.get_data(as_text=True)) == { + "code": "unauthorized", + "message": "Unauthorized.", + } diff --git a/api/tests/unit_tests/libs/test_login.py b/api/tests/unit_tests/libs/test_login.py index 0c9e73299b..2bf2212844 100644 --- a/api/tests/unit_tests/libs/test_login.py +++ b/api/tests/unit_tests/libs/test_login.py @@ -2,11 +2,12 @@ from types import SimpleNamespace from unittest.mock import MagicMock import pytest -from flask import Flask, g -from flask_login import LoginManager, UserMixin +from flask import Flask, Response, g +from flask_login import UserMixin from pytest_mock import MockerFixture import libs.login as login_module +from extensions.ext_login import DifyLoginManager from libs.login import current_user from models.account import Account @@ -39,9 +40,12 @@ def login_app(mocker: MockerFixture) -> Flask: app = Flask(__name__) app.config["TESTING"] = True - login_manager = LoginManager() + login_manager = DifyLoginManager() login_manager.init_app(app) - login_manager.unauthorized = mocker.Mock(name="unauthorized", return_value="Unauthorized") + login_manager.unauthorized = mocker.Mock( + name="unauthorized", + return_value=Response("Unauthorized", status=401, content_type="application/json"), + ) @login_manager.user_loader def load_user(_user_id: str): @@ -109,18 +113,43 @@ class TestLoginRequired: resolved_user: MockUser | None, description: str, ): - """Test that missing or unauthenticated users are redirected.""" + """Test that missing or unauthenticated users return the manager response.""" resolve_user = resolve_current_user(resolved_user) with login_app.test_request_context(): result = protected_view() - assert result == "Unauthorized", description + assert result is login_app.login_manager.unauthorized.return_value, description + assert isinstance(result, Response) + assert result.status_code == 401 resolve_user.assert_called_once_with() login_app.login_manager.unauthorized.assert_called_once_with() csrf_check.assert_not_called() + def test_unauthorized_access_propagates_response_object( + self, + login_app: Flask, + protected_view, + csrf_check: MagicMock, + resolve_current_user, + mocker: MockerFixture, + ) -> None: + """Test that unauthorized responses are propagated as Flask Response objects.""" + resolve_user = resolve_current_user(None) + response = Response("Unauthorized", status=401, content_type="application/json") + mocker.patch.object( + login_module, "_get_login_manager", return_value=SimpleNamespace(unauthorized=lambda: response) + ) + + with login_app.test_request_context(): + result = protected_view() + + assert result is response + assert isinstance(result, Response) + resolve_user.assert_called_once_with() + csrf_check.assert_not_called() + @pytest.mark.parametrize( ("method", "login_disabled"), [ @@ -168,10 +197,14 @@ class TestGetUser: """Test that _get_user loads user if not already in g.""" mock_user = MockUser("test_user") - def _load_user() -> None: + def load_user_from_request_context() -> None: g._login_user = mock_user - load_user = mocker.patch.object(login_app.login_manager, "_load_user", side_effect=_load_user) + load_user = mocker.patch.object( + login_app.login_manager, + "load_user_from_request_context", + side_effect=load_user_from_request_context, + ) with login_app.test_request_context(): user = login_module._get_user() diff --git a/api/tests/unit_tests/services/dataset_metadata.py b/api/tests/unit_tests/services/dataset_metadata.py index 5ba18d8dc0..b825a8686a 100644 --- a/api/tests/unit_tests/services/dataset_metadata.py +++ b/api/tests/unit_tests/services/dataset_metadata.py @@ -401,10 +401,7 @@ class TestMetadataServiceCreateMetadata: metadata_args = MetadataTestDataFactory.create_metadata_args_mock(name="category", metadata_type="string") # Mock query to return None (no existing metadata with same name) - mock_query = Mock() - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None - mock_db_session.query.return_value = mock_query + mock_db_session.scalar.return_value = None # Mock BuiltInField enum iteration with patch("services.metadata_service.BuiltInField") as mock_builtin: @@ -417,10 +414,6 @@ class TestMetadataServiceCreateMetadata: assert result is not None assert isinstance(result, DatasetMetadata) - # Verify query was made to check for duplicates - mock_db_session.query.assert_called() - mock_query.filter_by.assert_called() - # Verify metadata was added and committed mock_db_session.add.assert_called_once() mock_db_session.commit.assert_called_once() @@ -468,10 +461,7 @@ class TestMetadataServiceCreateMetadata: # Mock existing metadata with same name existing_metadata = MetadataTestDataFactory.create_metadata_mock(name="category") - mock_query = Mock() - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = existing_metadata - mock_db_session.query.return_value = mock_query + mock_db_session.scalar.return_value = existing_metadata # Act & Assert with pytest.raises(ValueError, match="Metadata name already exists"): @@ -500,10 +490,7 @@ class TestMetadataServiceCreateMetadata: ) # Mock query to return None (no duplicate in database) - mock_query = Mock() - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None - mock_db_session.query.return_value = mock_query + mock_db_session.scalar.return_value = None # Mock BuiltInField to include the conflicting name with patch("services.metadata_service.BuiltInField") as mock_builtin: @@ -597,27 +584,11 @@ class TestMetadataServiceUpdateMetadataName: existing_metadata = MetadataTestDataFactory.create_metadata_mock(metadata_id=metadata_id, name="category") - # Mock query for duplicate check (no duplicate) - mock_query = Mock() - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None - mock_db_session.query.return_value = mock_query - - # Mock metadata retrieval - def query_side_effect(model): - if model == DatasetMetadata: - mock_meta_query = Mock() - mock_meta_query.filter_by.return_value = mock_meta_query - mock_meta_query.first.return_value = existing_metadata - return mock_meta_query - return mock_query - - mock_db_session.query.side_effect = query_side_effect + # Mock scalar calls: first for duplicate check (None), second for metadata retrieval + mock_db_session.scalar.side_effect = [None, existing_metadata] # Mock no metadata bindings (no documents to update) - mock_binding_query = Mock() - mock_binding_query.filter_by.return_value = mock_binding_query - mock_binding_query.all.return_value = [] + mock_db_session.scalars.return_value.all.return_value = [] # Mock BuiltInField enum with patch("services.metadata_service.BuiltInField") as mock_builtin: @@ -655,22 +626,8 @@ class TestMetadataServiceUpdateMetadataName: metadata_id = "non-existent-metadata" new_name = "updated_category" - # Mock query for duplicate check (no duplicate) - mock_query = Mock() - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None - mock_db_session.query.return_value = mock_query - - # Mock metadata retrieval to return None - def query_side_effect(model): - if model == DatasetMetadata: - mock_meta_query = Mock() - mock_meta_query.filter_by.return_value = mock_meta_query - mock_meta_query.first.return_value = None # Not found - return mock_meta_query - return mock_query - - mock_db_session.query.side_effect = query_side_effect + # Mock scalar calls: first for duplicate check (None), second for metadata retrieval (None = not found) + mock_db_session.scalar.side_effect = [None, None] # Mock BuiltInField enum with patch("services.metadata_service.BuiltInField") as mock_builtin: @@ -746,15 +703,10 @@ class TestMetadataServiceDeleteMetadata: existing_metadata = MetadataTestDataFactory.create_metadata_mock(metadata_id=metadata_id, name="category") # Mock metadata retrieval - mock_query = Mock() - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = existing_metadata - mock_db_session.query.return_value = mock_query + mock_db_session.scalar.return_value = existing_metadata # Mock no metadata bindings (no documents to update) - mock_binding_query = Mock() - mock_binding_query.filter_by.return_value = mock_binding_query - mock_binding_query.all.return_value = [] + mock_db_session.scalars.return_value.all.return_value = [] # Act result = MetadataService.delete_metadata(dataset_id, metadata_id) @@ -788,10 +740,7 @@ class TestMetadataServiceDeleteMetadata: metadata_id = "non-existent-metadata" # Mock metadata retrieval to return None - mock_query = Mock() - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None - mock_db_session.query.return_value = mock_query + mock_db_session.scalar.return_value = None # Act & Assert with pytest.raises(ValueError, match="Metadata not found"): @@ -1013,10 +962,7 @@ class TestMetadataServiceGetDatasetMetadatas: ) # Mock usage count queries - mock_query = Mock() - mock_query.filter_by.return_value = mock_query - mock_query.count.return_value = 5 # 5 documents use this metadata - mock_db_session.query.return_value = mock_query + mock_db_session.scalar.return_value = 5 # 5 documents use this metadata # Act result = MetadataService.get_dataset_metadatas(dataset) diff --git a/api/tests/unit_tests/services/dataset_service_test_helpers.py b/api/tests/unit_tests/services/dataset_service_test_helpers.py index ef73bc0e01..da557de8a4 100644 --- a/api/tests/unit_tests/services/dataset_service_test_helpers.py +++ b/api/tests/unit_tests/services/dataset_service_test_helpers.py @@ -14,6 +14,7 @@ from graphon.model_runtime.entities.model_entities import ModelFeature, ModelTyp from werkzeug.exceptions import Forbidden, NotFound from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError +from core.rag.entities import PreProcessingRule, Rule, Segmentation from core.rag.index_processor.constant.built_in_field import BuiltInField from core.rag.index_processor.constant.index_type import IndexStructureType from core.rag.retrieval.retrieval_methods import RetrievalMethod @@ -44,12 +45,9 @@ from services.entities.knowledge_entities.knowledge_entities import ( NotionIcon, NotionInfo, NotionPage, - PreProcessingRule, ProcessRule, RerankingModel, RetrievalModel, - Rule, - Segmentation, SegmentUpdateArgs, WebsiteInfo, ) diff --git a/api/tests/unit_tests/services/document_service_validation.py b/api/tests/unit_tests/services/document_service_validation.py index 7c36e9d960..6903c47a24 100644 --- a/api/tests/unit_tests/services/document_service_validation.py +++ b/api/tests/unit_tests/services/document_service_validation.py @@ -112,6 +112,7 @@ import pytest from graphon.model_runtime.entities.model_entities import ModelType from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError +from core.rag.entities import PreProcessingRule, Rule, Segmentation from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType from models.dataset import Dataset, DatasetProcessRule, Document from services.dataset_service import DatasetService, DocumentService @@ -122,10 +123,7 @@ from services.entities.knowledge_entities.knowledge_entities import ( KnowledgeConfig, NotionInfo, NotionPage, - PreProcessingRule, ProcessRule, - Rule, - Segmentation, WebsiteInfo, ) diff --git a/api/tests/unit_tests/services/external_dataset_service.py b/api/tests/unit_tests/services/external_dataset_service.py index 70bd1c73b3..5848603ab8 100644 --- a/api/tests/unit_tests/services/external_dataset_service.py +++ b/api/tests/unit_tests/services/external_dataset_service.py @@ -292,7 +292,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi: """ api = Mock(spec=ExternalKnowledgeApis) - mock_db_session.query.return_value.filter_by.return_value.first.return_value = api + mock_db_session.scalar.return_value = api result = ExternalDatasetService.get_external_knowledge_api("api-id", "tenant-id") assert result is api @@ -302,7 +302,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi: When the record is absent, a ``ValueError`` is raised. """ - mock_db_session.query.return_value.filter_by.return_value.first.return_value = None + mock_db_session.scalar.return_value = None with pytest.raises(ValueError, match="api template not found"): ExternalDatasetService.get_external_knowledge_api("missing-id", "tenant-id") @@ -320,7 +320,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi: existing_api = Mock(spec=ExternalKnowledgeApis) existing_api.settings_dict = {"api_key": "stored-key"} existing_api.settings = '{"api_key":"stored-key"}' - mock_db_session.query.return_value.filter_by.return_value.first.return_value = existing_api + mock_db_session.scalar.return_value = existing_api args = { "name": "New Name", @@ -340,7 +340,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi: Updating a non‑existent API template should raise ``ValueError``. """ - mock_db_session.query.return_value.filter_by.return_value.first.return_value = None + mock_db_session.scalar.return_value = None with pytest.raises(ValueError, match="api template not found"): ExternalDatasetService.update_external_knowledge_api( @@ -356,7 +356,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi: """ api = Mock(spec=ExternalKnowledgeApis) - mock_db_session.query.return_value.filter_by.return_value.first.return_value = api + mock_db_session.scalar.return_value = api ExternalDatasetService.delete_external_knowledge_api("tenant-1", "api-1") @@ -368,7 +368,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi: Deletion of a missing template should raise ``ValueError``. """ - mock_db_session.query.return_value.filter_by.return_value.first.return_value = None + mock_db_session.scalar.return_value = None with pytest.raises(ValueError, match="api template not found"): ExternalDatasetService.delete_external_knowledge_api("tenant-1", "missing") @@ -394,7 +394,7 @@ class TestExternalDatasetServiceUsageAndBindings: When there are bindings, ``external_knowledge_api_use_check`` returns True and count. """ - mock_db_session.query.return_value.filter_by.return_value.count.return_value = 3 + mock_db_session.scalar.return_value = 3 in_use, count = ExternalDatasetService.external_knowledge_api_use_check("api-1") @@ -406,7 +406,7 @@ class TestExternalDatasetServiceUsageAndBindings: Zero bindings should return ``(False, 0)``. """ - mock_db_session.query.return_value.filter_by.return_value.count.return_value = 0 + mock_db_session.scalar.return_value = 0 in_use, count = ExternalDatasetService.external_knowledge_api_use_check("api-1") @@ -419,7 +419,7 @@ class TestExternalDatasetServiceUsageAndBindings: """ binding = Mock(spec=ExternalKnowledgeBindings) - mock_db_session.query.return_value.filter_by.return_value.first.return_value = binding + mock_db_session.scalar.return_value = binding result = ExternalDatasetService.get_external_knowledge_binding_with_dataset_id("tenant-1", "ds-1") assert result is binding @@ -429,7 +429,7 @@ class TestExternalDatasetServiceUsageAndBindings: Missing binding should result in a ``ValueError``. """ - mock_db_session.query.return_value.filter_by.return_value.first.return_value = None + mock_db_session.scalar.return_value = None with pytest.raises(ValueError, match="external knowledge binding not found"): ExternalDatasetService.get_external_knowledge_binding_with_dataset_id("tenant-1", "ds-1") @@ -460,7 +460,7 @@ class TestExternalDatasetServiceDocumentCreateArgsValidate: '[{"document_process_setting":[{"name":"foo","required":true},{"name":"bar","required":false}]}]' ) # Raw string; the service itself calls json.loads on it - mock_db_session.query.return_value.filter_by.return_value.first.return_value = external_api + mock_db_session.scalar.return_value = external_api process_parameter = {"foo": "value", "bar": "optional"} @@ -474,7 +474,7 @@ class TestExternalDatasetServiceDocumentCreateArgsValidate: When the referenced API template is missing, a ``ValueError`` is raised. """ - mock_db_session.query.return_value.filter_by.return_value.first.return_value = None + mock_db_session.scalar.return_value = None with pytest.raises(ValueError, match="api template not found"): ExternalDatasetService.document_create_args_validate("tenant-1", "missing", {}) @@ -488,7 +488,7 @@ class TestExternalDatasetServiceDocumentCreateArgsValidate: external_api.settings = ( '[{"document_process_setting":[{"name":"foo","required":true},{"name":"bar","required":false}]}]' ) - mock_db_session.query.return_value.filter_by.return_value.first.return_value = external_api + mock_db_session.scalar.return_value = external_api process_parameter = {"bar": "present"} # missing "foo" @@ -702,7 +702,7 @@ class TestExternalDatasetServiceCreateExternalDataset: } # No existing dataset with same name. - mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [ + mock_db_session.scalar.side_effect = [ None, # duplicate‑name check Mock(spec=ExternalKnowledgeApis), # external knowledge api ] @@ -724,7 +724,7 @@ class TestExternalDatasetServiceCreateExternalDataset: """ existing_dataset = Mock(spec=Dataset) - mock_db_session.query.return_value.filter_by.return_value.first.return_value = existing_dataset + mock_db_session.scalar.return_value = existing_dataset args = { "name": "Existing", @@ -744,7 +744,7 @@ class TestExternalDatasetServiceCreateExternalDataset: """ # First call: duplicate name check – not found. - mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [ + mock_db_session.scalar.side_effect = [ None, None, # external knowledge api lookup ] @@ -763,8 +763,10 @@ class TestExternalDatasetServiceCreateExternalDataset: ``external_knowledge_id`` and ``external_knowledge_api_id`` are mandatory. """ - # duplicate name check - mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [ + # duplicate name check — two calls to create_external_dataset, each does 2 scalar calls + mock_db_session.scalar.side_effect = [ + None, + Mock(spec=ExternalKnowledgeApis), None, Mock(spec=ExternalKnowledgeApis), ] @@ -826,7 +828,7 @@ class TestExternalDatasetServiceFetchExternalKnowledgeRetrieval: api.settings = '{"endpoint":"https://example.com","api_key":"secret"}' # First query: binding; second query: api. - mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [ + mock_db_session.scalar.side_effect = [ binding, api, ] @@ -861,7 +863,7 @@ class TestExternalDatasetServiceFetchExternalKnowledgeRetrieval: Missing binding should raise ``ValueError``. """ - mock_db_session.query.return_value.filter_by.return_value.first.return_value = None + mock_db_session.scalar.return_value = None with pytest.raises(ValueError, match="external knowledge binding not found"): ExternalDatasetService.fetch_external_knowledge_retrieval( @@ -878,7 +880,7 @@ class TestExternalDatasetServiceFetchExternalKnowledgeRetrieval: """ binding = ExternalDatasetTestDataFactory.create_external_binding() - mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [ + mock_db_session.scalar.side_effect = [ binding, None, ] @@ -901,7 +903,7 @@ class TestExternalDatasetServiceFetchExternalKnowledgeRetrieval: api = Mock(spec=ExternalKnowledgeApis) api.settings = '{"endpoint":"https://example.com","api_key":"secret"}' - mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [ + mock_db_session.scalar.side_effect = [ binding, api, ] diff --git a/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_service.py b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_service.py index cb3c2d742d..f270ee0fde 100644 --- a/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_service.py +++ b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_service.py @@ -117,9 +117,7 @@ def test_get_all_published_workflow_applies_limit_and_has_more(rag_pipeline_serv def test_get_pipeline_raises_when_dataset_not_found(mocker, rag_pipeline_service) -> None: - first_query = mocker.Mock() - first_query.where.return_value.first.return_value = None - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=first_query) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=None) with pytest.raises(ValueError, match="Dataset not found"): rag_pipeline_service.get_pipeline("tenant-1", "dataset-1") @@ -131,12 +129,8 @@ def test_get_pipeline_raises_when_dataset_not_found(mocker, rag_pipeline_service def test_update_customized_pipeline_template_success(mocker) -> None: template = SimpleNamespace(name="old", description="old", icon={}, updated_by=None) - # First query finds the template, second query (duplicate check) returns None - query_mock_1 = mocker.Mock() - query_mock_1.where.return_value.first.return_value = template - query_mock_2 = mocker.Mock() - query_mock_2.where.return_value.first.return_value = None - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", side_effect=[query_mock_1, query_mock_2]) + # First scalar finds the template, second scalar (duplicate check) returns None + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[template, None]) mocker.patch("services.rag_pipeline.rag_pipeline.db.session.commit") mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1")) @@ -152,9 +146,7 @@ def test_update_customized_pipeline_template_success(mocker) -> None: def test_update_customized_pipeline_template_not_found(mocker) -> None: - query_mock = mocker.Mock() - query_mock.where.return_value.first.return_value = None - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=None) mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1")) info = PipelineTemplateInfoEntity(name="x", description="d", icon_info=IconInfo(icon="i")) @@ -166,9 +158,7 @@ def test_update_customized_pipeline_template_duplicate_name(mocker) -> None: template = SimpleNamespace(name="old", description="old", icon={}, updated_by=None) duplicate = SimpleNamespace(name="dup") - query_mock = mocker.Mock() - query_mock.where.return_value.first.side_effect = [template, duplicate] - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[template, duplicate]) mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1")) info = PipelineTemplateInfoEntity(name="dup", description="d", icon_info=IconInfo(icon="i")) @@ -181,9 +171,7 @@ def test_update_customized_pipeline_template_duplicate_name(mocker) -> None: def test_delete_customized_pipeline_template_success(mocker) -> None: template = SimpleNamespace(id="tpl-1") - query_mock = mocker.Mock() - query_mock.where.return_value.first.return_value = template - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=template) delete_mock = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.delete") commit_mock = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.commit") @@ -196,9 +184,7 @@ def test_delete_customized_pipeline_template_success(mocker) -> None: def test_delete_customized_pipeline_template_not_found(mocker) -> None: - query_mock = mocker.Mock() - query_mock.where.return_value.first.return_value = None - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=None) mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1")) with pytest.raises(ValueError, match="Customized pipeline template not found"): @@ -397,18 +383,14 @@ def test_get_rag_pipeline_workflow_run_delegates(mocker, rag_pipeline_service) - def test_is_workflow_exist_returns_true_when_draft_exists(mocker, rag_pipeline_service) -> None: - query_mock = mocker.Mock() - query_mock.where.return_value.count.return_value = 1 - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=1) pipeline = SimpleNamespace(tenant_id="t1", id="p1") assert rag_pipeline_service.is_workflow_exist(pipeline) is True def test_is_workflow_exist_returns_false_when_no_draft(mocker, rag_pipeline_service) -> None: - query_mock = mocker.Mock() - query_mock.where.return_value.count.return_value = 0 - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=0) pipeline = SimpleNamespace(tenant_id="t1", id="p1") assert rag_pipeline_service.is_workflow_exist(pipeline) is False @@ -738,8 +720,7 @@ def test_get_second_step_parameters_success(mocker, rag_pipeline_service) -> Non def test_publish_customized_pipeline_template_success(mocker, rag_pipeline_service) -> None: - from models.dataset import Dataset, Pipeline, PipelineCustomizedTemplate - from models.workflow import Workflow + from models.dataset import Pipeline # 1. Setup mocks pipeline = mocker.Mock(spec=Pipeline) @@ -754,36 +735,15 @@ def test_publish_customized_pipeline_template_success(mocker, rag_pipeline_servi # Mock db itself to avoid app context errors mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db") - # Improved mocking for session.query - def mock_query_side_effect(model): - m = mocker.Mock() - if model == Pipeline: - m.where.return_value.first.return_value = pipeline - elif model == Workflow: - m.where.return_value.first.return_value = workflow - elif model == PipelineCustomizedTemplate: - m.where.return_value.first.return_value = None - elif model == Dataset: - m.where.return_value.first.return_value = mocker.Mock() - else: - # For func.max cases - m.where.return_value.scalar.return_value = 5 - m.where.return_value.first.return_value = mocker.Mock() - return m - - mock_db.session.query.side_effect = mock_query_side_effect + # Mock get() for Pipeline and Workflow PK lookups + mock_db.session.get.side_effect = [pipeline, workflow] + # Mock scalar() for template name check (None) and max position (5) + mock_db.session.scalar.side_effect = [None, 5] # Mock retrieve_dataset dataset = mocker.Mock() pipeline.retrieve_dataset.return_value = dataset - # Mock max position - mocker.patch("services.rag_pipeline.rag_pipeline.func.max", return_value=1) - mocker.patch( - "services.rag_pipeline.rag_pipeline.db.session.query.return_value.where.return_value.scalar", - return_value=5, - ) - # Mock RagPipelineDslService mock_dsl_service = mocker.Mock() mock_dsl_service.export_rag_pipeline_dsl.return_value = {"dsl": "content"} @@ -839,9 +799,7 @@ def test_get_datasource_plugins_success(mocker, rag_pipeline_service) -> None: workflow.rag_pipeline_variables = [] # Mock queries - mock_query = mocker.Mock() - mock_query.where.return_value.first.side_effect = [dataset, pipeline] - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=mock_query) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline]) mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) @@ -881,11 +839,9 @@ def test_retry_error_document_success(mocker, rag_pipeline_service) -> None: workflow = mocker.Mock() - # Mock queries - mock_query = mocker.Mock() - # Log lookup, then Pipeline lookup - mock_query.where.return_value.first.side_effect = [log, pipeline] - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=mock_query) + # Mock queries: Log lookup via scalar, Pipeline lookup via get + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=log) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=pipeline) mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) @@ -913,7 +869,7 @@ def test_set_datasource_variables_success(mocker, rag_pipeline_service) -> None: # Mock db aggressively mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db") mock_db.engine = mocker.Mock() - mock_db.session.query.return_value.where.return_value.first.return_value = mocker.Mock() + mock_db.session.scalar.return_value = mocker.Mock() pipeline = mocker.Mock(spec=Pipeline) pipeline.id = "p-1" @@ -976,7 +932,7 @@ def test_get_draft_workflow_success(mocker, rag_pipeline_service) -> None: workflow = mocker.Mock(spec=Workflow) mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db") - mock_db.session.query.return_value.where.return_value.first.return_value = workflow + mock_db.session.scalar.return_value = workflow # 2. Run test result = rag_pipeline_service.get_draft_workflow(pipeline) @@ -998,7 +954,7 @@ def test_get_published_workflow_success(mocker, rag_pipeline_service) -> None: workflow = mocker.Mock(spec=Workflow) mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db") - mock_db.session.query.return_value.where.return_value.first.return_value = workflow + mock_db.session.scalar.return_value = workflow # 2. Run test result = rag_pipeline_service.get_published_workflow(pipeline) @@ -1319,11 +1275,8 @@ def test_get_rag_pipeline_workflow_run_node_executions_returns_sorted_executions def test_get_recommended_plugins_returns_empty_when_no_active_plugins(mocker, rag_pipeline_service) -> None: - query = mocker.Mock() - query.where.return_value = query - query.order_by.return_value.all.return_value = [] mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db") - mock_db.session.query.return_value = query + mock_db.session.scalars.return_value.all.return_value = [] result = rag_pipeline_service.get_recommended_plugins("all") @@ -1336,11 +1289,8 @@ def test_get_recommended_plugins_returns_empty_when_no_active_plugins(mocker, ra def test_get_recommended_plugins_returns_installed_and_uninstalled(mocker, rag_pipeline_service) -> None: plugin_a = SimpleNamespace(plugin_id="plugin-a") plugin_b = SimpleNamespace(plugin_id="plugin-b") - query = mocker.Mock() - query.where.return_value = query - query.order_by.return_value.all.return_value = [plugin_a, plugin_b] mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db") - mock_db.session.query.return_value = query + mock_db.session.scalars.return_value.all.return_value = [plugin_a, plugin_b] mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1")) mocker.patch( "services.rag_pipeline.rag_pipeline.BuiltinToolManageService.list_builtin_tools", @@ -1568,9 +1518,7 @@ def test_get_second_step_parameters_filters_first_step_variables(mocker, rag_pip def test_retry_error_document_raises_when_execution_log_not_found(mocker, rag_pipeline_service) -> None: - query = mocker.Mock() - query.where.return_value.first.return_value = None - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=None) with pytest.raises(ValueError, match="Document pipeline execution log not found"): rag_pipeline_service.retry_error_document( @@ -1581,9 +1529,7 @@ def test_retry_error_document_raises_when_execution_log_not_found(mocker, rag_pi def test_get_datasource_plugins_raises_when_workflow_not_found(mocker, rag_pipeline_service) -> None: dataset = SimpleNamespace(pipeline_id="p1") pipeline = SimpleNamespace(id="p1", tenant_id="t1") - query = mocker.Mock() - query.where.return_value.first.side_effect = [dataset, pipeline] - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline]) mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=None) with pytest.raises(ValueError, match="Pipeline or workflow not found"): @@ -1656,8 +1602,7 @@ def test_handle_node_run_result_marks_document_error_for_published_invoke(mocker document = SimpleNamespace(indexing_status="waiting", error=None) query = mocker.Mock() - query.where.return_value.first.return_value = document - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=document) add_mock = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.add") commit_mock = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.commit") @@ -1712,9 +1657,7 @@ def test_run_datasource_node_preview_raises_for_unsupported_provider(mocker, rag def test_publish_customized_pipeline_template_raises_for_missing_pipeline(mocker, rag_pipeline_service) -> None: - query = mocker.Mock() - query.where.return_value.first.return_value = None - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=None) with pytest.raises(ValueError, match="Pipeline not found"): rag_pipeline_service.publish_customized_pipeline_template("p1", {}) @@ -1722,9 +1665,7 @@ def test_publish_customized_pipeline_template_raises_for_missing_pipeline(mocker def test_publish_customized_pipeline_template_raises_for_missing_workflow_id(mocker, rag_pipeline_service) -> None: pipeline = SimpleNamespace(id="p1", tenant_id="t1", workflow_id=None) - query = mocker.Mock() - query.where.return_value.first.return_value = pipeline - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=pipeline) with pytest.raises(ValueError, match="Pipeline workflow not found"): rag_pipeline_service.publish_customized_pipeline_template("p1", {"name": "template-name"}) @@ -1732,8 +1673,7 @@ def test_publish_customized_pipeline_template_raises_for_missing_workflow_id(moc def test_get_pipeline_raises_when_dataset_missing(mocker, rag_pipeline_service) -> None: query = mocker.Mock() - query.where.return_value.first.return_value = None - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=None) with pytest.raises(ValueError, match="Dataset not found"): rag_pipeline_service.get_pipeline("t1", "d1") @@ -1742,8 +1682,7 @@ def test_get_pipeline_raises_when_dataset_missing(mocker, rag_pipeline_service) def test_get_pipeline_raises_when_pipeline_missing(mocker, rag_pipeline_service) -> None: dataset = SimpleNamespace(pipeline_id="p1") query = mocker.Mock() - query.where.return_value.first.side_effect = [dataset, None] - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, None]) with pytest.raises(ValueError, match="Pipeline not found"): rag_pipeline_service.get_pipeline("t1", "d1") @@ -1783,8 +1722,7 @@ def test_get_pipeline_templates_builtin_en_us_no_fallback(mocker) -> None: def test_update_customized_pipeline_template_commits_when_name_empty(mocker) -> None: template = SimpleNamespace(name="old", description="old", icon={}, updated_by=None) query = mocker.Mock() - query.where.return_value.first.return_value = template - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=template) commit = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.commit") mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1")) @@ -2011,8 +1949,7 @@ def test_run_free_workflow_node_delegates_to_handle_result(mocker, rag_pipeline_ def test_publish_customized_pipeline_template_raises_when_workflow_missing(mocker, rag_pipeline_service) -> None: pipeline = SimpleNamespace(id="p1", tenant_id="t1", workflow_id="wf-1") query = mocker.Mock() - query.where.return_value.first.side_effect = [pipeline, None] - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", side_effect=[pipeline, None]) with pytest.raises(ValueError, match="Workflow not found"): rag_pipeline_service.publish_customized_pipeline_template("p1", {}) @@ -2021,11 +1958,9 @@ def test_publish_customized_pipeline_template_raises_when_workflow_missing(mocke def test_publish_customized_pipeline_template_raises_when_dataset_missing(mocker, rag_pipeline_service) -> None: pipeline = SimpleNamespace(id="p1", tenant_id="t1", workflow_id="wf-1") workflow = SimpleNamespace(id="wf-1") - query = mocker.Mock() - query.where.return_value.first.side_effect = [pipeline, workflow] mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db") mock_db.engine = mocker.Mock() - mock_db.session.query.return_value = query + mock_db.session.get.side_effect = [pipeline, workflow] session_ctx = mocker.MagicMock() session_ctx.__enter__.return_value = SimpleNamespace() session_ctx.__exit__.return_value = False @@ -2038,11 +1973,8 @@ def test_publish_customized_pipeline_template_raises_when_dataset_missing(mocker def test_get_recommended_plugins_skips_manifest_when_missing(mocker, rag_pipeline_service) -> None: plugin = SimpleNamespace(plugin_id="plugin-a") - query = mocker.Mock() - query.where.return_value = query - query.order_by.return_value.all.return_value = [plugin] mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db") - mock_db.session.query.return_value = query + mock_db.session.scalars.return_value.all.return_value = [plugin] mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1")) mocker.patch("services.rag_pipeline.rag_pipeline.BuiltinToolManageService.list_builtin_tools", return_value=[]) mocker.patch("services.rag_pipeline.rag_pipeline.marketplace.batch_fetch_plugin_by_ids", return_value=[]) @@ -2056,8 +1988,8 @@ def test_get_recommended_plugins_skips_manifest_when_missing(mocker, rag_pipelin def test_retry_error_document_raises_when_pipeline_missing(mocker, rag_pipeline_service) -> None: exec_log = SimpleNamespace(pipeline_id="p1") query = mocker.Mock() - query.where.return_value.first.side_effect = [exec_log, None] - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=exec_log) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=None) with pytest.raises(ValueError, match="Pipeline not found"): rag_pipeline_service.retry_error_document( @@ -2069,8 +2001,8 @@ def test_retry_error_document_raises_when_workflow_missing(mocker, rag_pipeline_ exec_log = SimpleNamespace(pipeline_id="p1") pipeline = SimpleNamespace(id="p1") query = mocker.Mock() - query.where.return_value.first.side_effect = [exec_log, pipeline] - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=exec_log) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=pipeline) mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=None) with pytest.raises(ValueError, match="Workflow not found"): @@ -2086,8 +2018,7 @@ def test_get_datasource_plugins_returns_empty_for_non_datasource_nodes(mocker, r graph_dict={"nodes": [{"id": "n1", "data": {"type": "start"}}]}, rag_pipeline_variables=[] ) query = mocker.Mock() - query.where.return_value.first.side_effect = [dataset, pipeline] - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline]) mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) assert rag_pipeline_service.get_datasource_plugins("t1", "d1", True) == [] @@ -2250,8 +2181,7 @@ def test_get_datasource_plugins_handles_empty_datasource_data_and_non_published( rag_pipeline_variables=[{"variable": "v1", "belong_to_node_id": "shared"}], ) query = mocker.Mock() - query.where.return_value.first.side_effect = [dataset, pipeline] - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline]) mocker.patch.object(rag_pipeline_service, "get_draft_workflow", return_value=workflow) mocker.patch( "services.rag_pipeline.rag_pipeline.DatasourceProviderService.list_datasource_credentials", return_value=[] @@ -2291,8 +2221,7 @@ def test_get_datasource_plugins_extracts_user_inputs_and_credentials(mocker, rag ], ) query = mocker.Mock() - query.where.return_value.first.side_effect = [dataset, pipeline] - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline]) mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) mocker.patch( "services.rag_pipeline.rag_pipeline.DatasourceProviderService.list_datasource_credentials", @@ -2310,8 +2239,7 @@ def test_get_pipeline_returns_pipeline_when_found(mocker, rag_pipeline_service) dataset = SimpleNamespace(pipeline_id="p1") pipeline = SimpleNamespace(id="p1") query = mocker.Mock() - query.where.return_value.first.side_effect = [dataset, pipeline] - mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline]) result = rag_pipeline_service.get_pipeline("t1", "d1") diff --git a/api/tests/unit_tests/services/test_account_service.py b/api/tests/unit_tests/services/test_account_service.py index dcd6785464..d15074e7a6 100644 --- a/api/tests/unit_tests/services/test_account_service.py +++ b/api/tests/unit_tests/services/test_account_service.py @@ -173,9 +173,7 @@ class TestAccountService: # Setup test data mock_account = TestAccountAssociatedDataFactory.create_account_mock() - # Setup smart database query mock - query_results = {("Account", "email", "test@example.com"): mock_account} - ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + mock_db_dependencies["db"].session.scalar.return_value = mock_account mock_password_dependencies["compare_password"].return_value = True @@ -188,9 +186,7 @@ class TestAccountService: def test_authenticate_account_not_found(self, mock_db_dependencies): """Test authentication when account does not exist.""" - # Setup smart database query mock - no matching results - query_results = {("Account", "email", "notfound@example.com"): None} - ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + mock_db_dependencies["db"].session.scalar.return_value = None # Execute test and verify exception self._assert_exception_raised( @@ -202,9 +198,7 @@ class TestAccountService: # Setup test data mock_account = TestAccountAssociatedDataFactory.create_account_mock(status="banned") - # Setup smart database query mock - query_results = {("Account", "email", "banned@example.com"): mock_account} - ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + mock_db_dependencies["db"].session.scalar.return_value = mock_account # Execute test and verify exception self._assert_exception_raised(AccountLoginError, AccountService.authenticate, "banned@example.com", "password") @@ -214,9 +208,7 @@ class TestAccountService: # Setup test data mock_account = TestAccountAssociatedDataFactory.create_account_mock() - # Setup smart database query mock - query_results = {("Account", "email", "test@example.com"): mock_account} - ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + mock_db_dependencies["db"].session.scalar.return_value = mock_account mock_password_dependencies["compare_password"].return_value = False @@ -230,9 +222,7 @@ class TestAccountService: # Setup test data mock_account = TestAccountAssociatedDataFactory.create_account_mock(status="pending") - # Setup smart database query mock - query_results = {("Account", "email", "pending@example.com"): mock_account} - ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + mock_db_dependencies["db"].session.scalar.return_value = mock_account mock_password_dependencies["compare_password"].return_value = True @@ -422,12 +412,8 @@ class TestAccountService: mock_account = TestAccountAssociatedDataFactory.create_account_mock() mock_tenant_join = TestAccountAssociatedDataFactory.create_tenant_join_mock() - # Setup smart database query mock - query_results = { - ("Account", "id", "user-123"): mock_account, - ("TenantAccountJoin", "account_id", "user-123"): mock_tenant_join, - } - ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + mock_db_dependencies["db"].session.get.return_value = mock_account + mock_db_dependencies["db"].session.scalar.return_value = mock_tenant_join # Mock datetime with patch("services.account_service.datetime") as mock_datetime: @@ -444,9 +430,7 @@ class TestAccountService: def test_load_user_not_found(self, mock_db_dependencies): """Test user loading when user does not exist.""" - # Setup smart database query mock - no matching results - query_results = {("Account", "id", "non-existent-user"): None} - ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + mock_db_dependencies["db"].session.get.return_value = None # Execute test result = AccountService.load_user("non-existent-user") @@ -459,9 +443,7 @@ class TestAccountService: # Setup test data mock_account = TestAccountAssociatedDataFactory.create_account_mock(status="banned") - # Setup smart database query mock - query_results = {("Account", "id", "user-123"): mock_account} - ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + mock_db_dependencies["db"].session.get.return_value = mock_account # Execute test and verify exception self._assert_exception_raised( @@ -476,13 +458,9 @@ class TestAccountService: mock_account = TestAccountAssociatedDataFactory.create_account_mock() mock_available_tenant = TestAccountAssociatedDataFactory.create_tenant_join_mock(current=False) - # Setup smart database query mock for complex scenario - query_results = { - ("Account", "id", "user-123"): mock_account, - ("TenantAccountJoin", "account_id", "user-123"): None, # No current tenant - ("TenantAccountJoin", "order_by", "first_available"): mock_available_tenant, # First available tenant - } - ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + mock_db_dependencies["db"].session.get.return_value = mock_account + # First scalar: current tenant (None), second scalar: available tenant + mock_db_dependencies["db"].session.scalar.side_effect = [None, mock_available_tenant] # Mock datetime with patch("services.account_service.datetime") as mock_datetime: @@ -503,13 +481,9 @@ class TestAccountService: # Setup test data mock_account = TestAccountAssociatedDataFactory.create_account_mock() - # Setup smart database query mock for no tenants scenario - query_results = { - ("Account", "id", "user-123"): mock_account, - ("TenantAccountJoin", "account_id", "user-123"): None, # No current tenant - ("TenantAccountJoin", "order_by", "first_available"): None, # No available tenants - } - ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + mock_db_dependencies["db"].session.get.return_value = mock_account + # First scalar: current tenant (None), second scalar: available tenant (None) + mock_db_dependencies["db"].session.scalar.side_effect = [None, None] # Mock datetime with patch("services.account_service.datetime") as mock_datetime: @@ -582,12 +556,8 @@ class TestTenantService: # Setup test data mock_account = TestAccountAssociatedDataFactory.create_account_mock() - # Setup smart database query mock - no existing tenant joins - query_results = { - ("TenantAccountJoin", "account_id", "user-123"): None, - ("TenantAccountJoin", "tenant_id", "tenant-456"): None, # For has_roles check - } - ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + # Mock scalar to return None (no existing tenant joins) + mock_db_dependencies["db"].session.scalar.return_value = None # Setup external service mocks mock_external_service_dependencies[ @@ -676,9 +646,8 @@ class TestTenantService: mock_tenant.id = "tenant-456" mock_account = TestAccountAssociatedDataFactory.create_account_mock() - # Setup smart database query mock - no existing member - query_results = {("TenantAccountJoin", "tenant_id", "tenant-456"): None} - ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + # Mock scalar to return None (no existing member) + mock_db_dependencies["db"].session.scalar.return_value = None # Mock database operations mock_db_dependencies["db"].session.add = MagicMock() @@ -719,16 +688,8 @@ class TestTenantService: tenant_id="tenant-456", account_id="operator-123", role="owner" ) - query_mock_permission = MagicMock() - query_mock_permission.filter_by.return_value.first.return_value = mock_operator_join - - query_mock_ta = MagicMock() - query_mock_ta.filter_by.return_value.first.return_value = mock_ta - - query_mock_count = MagicMock() - query_mock_count.filter_by.return_value.count.return_value = 0 - - mock_db.session.query.side_effect = [query_mock_permission, query_mock_ta, query_mock_count] + # scalar calls: permission check, ta lookup, remaining count + mock_db.session.scalar.side_effect = [mock_operator_join, mock_ta, 0] with patch("services.enterprise.account_deletion_sync.sync_workspace_member_removal") as mock_sync: mock_sync.return_value = True @@ -767,17 +728,8 @@ class TestTenantService: tenant_id="tenant-456", account_id="operator-123", role="owner" ) - query_mock_permission = MagicMock() - query_mock_permission.filter_by.return_value.first.return_value = mock_operator_join - - query_mock_ta = MagicMock() - query_mock_ta.filter_by.return_value.first.return_value = mock_ta - - # Remaining join count = 1 (still in another workspace) - query_mock_count = MagicMock() - query_mock_count.filter_by.return_value.count.return_value = 1 - - mock_db.session.query.side_effect = [query_mock_permission, query_mock_ta, query_mock_count] + # scalar calls: permission check, ta lookup, remaining count = 1 + mock_db.session.scalar.side_effect = [mock_operator_join, mock_ta, 1] with patch("services.enterprise.account_deletion_sync.sync_workspace_member_removal") as mock_sync: mock_sync.return_value = True @@ -807,13 +759,8 @@ class TestTenantService: tenant_id="tenant-456", account_id="operator-123", role="owner" ) - query_mock_permission = MagicMock() - query_mock_permission.filter_by.return_value.first.return_value = mock_operator_join - - query_mock_ta = MagicMock() - query_mock_ta.filter_by.return_value.first.return_value = mock_ta - - mock_db.session.query.side_effect = [query_mock_permission, query_mock_ta] + # scalar calls: permission check, ta lookup (no count needed for active member) + mock_db.session.scalar.side_effect = [mock_operator_join, mock_ta] with patch("services.enterprise.account_deletion_sync.sync_workspace_member_removal") as mock_sync: mock_sync.return_value = True @@ -836,13 +783,8 @@ class TestTenantService: # Mock the complex query in switch_tenant method with patch("services.account_service.db") as mock_db: - # Mock the join query that returns the tenant_account_join - mock_query = MagicMock() - mock_where = MagicMock() - mock_where.first.return_value = mock_tenant_join - mock_query.where.return_value = mock_where - mock_query.join.return_value = mock_query - mock_db.session.query.return_value = mock_query + # Mock scalar for the join query + mock_db.session.scalar.return_value = mock_tenant_join # Execute test TenantService.switch_tenant(mock_account, "tenant-456") @@ -877,20 +819,8 @@ class TestTenantService: # Mock the database queries in update_member_role method with patch("services.account_service.db") as mock_db: - # Mock the first query for operator permission check - mock_query1 = MagicMock() - mock_filter1 = MagicMock() - mock_filter1.first.return_value = mock_operator_join - mock_query1.filter_by.return_value = mock_filter1 - - # Mock the second query for target member - mock_query2 = MagicMock() - mock_filter2 = MagicMock() - mock_filter2.first.return_value = mock_target_join - mock_query2.filter_by.return_value = mock_filter2 - - # Make the query method return different mocks for different calls - mock_db.session.query.side_effect = [mock_query1, mock_query2] + # scalar calls: permission check, target member lookup + mock_db.session.scalar.side_effect = [mock_operator_join, mock_target_join] # Execute test TenantService.update_member_role(mock_tenant, mock_member, "admin", mock_operator) @@ -912,9 +842,7 @@ class TestTenantService: tenant_id="tenant-456", account_id="operator-123", role="owner" ) - # Setup smart database query mock - query_results = {("TenantAccountJoin", "tenant_id", "tenant-456"): mock_operator_join} - ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + mock_db_dependencies["db"].session.scalar.return_value = mock_operator_join # Execute test - should not raise exception TenantService.check_member_permission(mock_tenant, mock_operator, mock_member, "add") @@ -1060,7 +988,7 @@ class TestRegisterService: ) # Verify rollback operations were called - mock_db_dependencies["db"].session.query.assert_called() + mock_db_dependencies["db"].session.execute.assert_called() # ==================== Registration Tests ==================== @@ -1499,16 +1427,18 @@ class TestRegisterService: mock_tenant.name = "Test Workspace" mock_inviter = TestAccountAssociatedDataFactory.create_account_mock(account_id="inviter-123", name="Inviter") - # Mock database queries - need to mock the Session query + # Mock database queries - need to mock the sessionmaker query mock_session = MagicMock() mock_session.query.return_value.filter_by.return_value.first.return_value = None # No existing account + mock_sessionmaker = MagicMock() + mock_sessionmaker.return_value.begin.return_value.__enter__.return_value = mock_session + mock_sessionmaker.return_value.begin.return_value.__exit__.return_value = None + with ( - patch("services.account_service.Session") as mock_session_class, + patch("services.account_service.sessionmaker", mock_sessionmaker), patch("services.account_service.AccountService.get_account_by_email_with_case_fallback") as mock_lookup, ): - mock_session_class.return_value.__enter__.return_value = mock_session - mock_session_class.return_value.__exit__.return_value = None mock_lookup.return_value = None # Mock RegisterService.register @@ -1557,12 +1487,14 @@ class TestRegisterService: mixed_email = "Invitee@Example.com" mock_session = MagicMock() + mock_sessionmaker = MagicMock() + mock_sessionmaker.return_value.begin.return_value.__enter__.return_value = mock_session + mock_sessionmaker.return_value.begin.return_value.__exit__.return_value = None + with ( - patch("services.account_service.Session") as mock_session_class, + patch("services.account_service.sessionmaker", mock_sessionmaker), patch("services.account_service.AccountService.get_account_by_email_with_case_fallback") as mock_lookup, ): - mock_session_class.return_value.__enter__.return_value = mock_session - mock_session_class.return_value.__exit__.return_value = None mock_lookup.return_value = None mock_new_account = TestAccountAssociatedDataFactory.create_account_mock( @@ -1613,22 +1545,22 @@ class TestRegisterService: account_id="existing-user-456", email="existing@example.com", status="pending" ) - # Mock database queries - need to mock the Session query + # Mock database queries - need to mock the sessionmaker query mock_session = MagicMock() mock_session.query.return_value.filter_by.return_value.first.return_value = mock_existing_account + mock_sessionmaker = MagicMock() + mock_sessionmaker.return_value.begin.return_value.__enter__.return_value = mock_session + mock_sessionmaker.return_value.begin.return_value.__exit__.return_value = None + with ( - patch("services.account_service.Session") as mock_session_class, + patch("services.account_service.sessionmaker", mock_sessionmaker), patch("services.account_service.AccountService.get_account_by_email_with_case_fallback") as mock_lookup, ): - mock_session_class.return_value.__enter__.return_value = mock_session - mock_session_class.return_value.__exit__.return_value = None mock_lookup.return_value = mock_existing_account - # Mock the db.session.query for TenantAccountJoin - mock_db_query = MagicMock() - mock_db_query.filter_by.return_value.first.return_value = None # No existing member - mock_db_dependencies["db"].session.query.return_value = mock_db_query + # Mock scalar for TenantAccountJoin lookup - no existing member + mock_db_dependencies["db"].session.scalar.return_value = None # Mock TenantService methods with ( @@ -1803,14 +1735,9 @@ class TestRegisterService: } mock_get_invitation_by_token.return_value = invitation_data - # Mock database queries - complex query mocking - mock_query1 = MagicMock() - mock_query1.where.return_value.first.return_value = mock_tenant - - mock_query2 = MagicMock() - mock_query2.join.return_value.where.return_value.first.return_value = (mock_account, "normal") - - mock_db_dependencies["db"].session.query.side_effect = [mock_query1, mock_query2] + # Mock scalar for tenant lookup, execute for account+role lookup + mock_db_dependencies["db"].session.scalar.return_value = mock_tenant + mock_db_dependencies["db"].session.execute.return_value.first.return_value = (mock_account, "normal") # Execute test result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") @@ -1842,10 +1769,8 @@ class TestRegisterService: } mock_redis_dependencies.get.return_value = json.dumps(invitation_data).encode() - # Mock database queries - no tenant found - mock_query = MagicMock() - mock_query.filter.return_value.first.return_value = None - mock_db_dependencies["db"].session.query.return_value = mock_query + # Mock scalar for tenant lookup - not found + mock_db_dependencies["db"].session.scalar.return_value = None # Execute test result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") @@ -1868,14 +1793,9 @@ class TestRegisterService: } mock_redis_dependencies.get.return_value = json.dumps(invitation_data).encode() - # Mock database queries - mock_query1 = MagicMock() - mock_query1.filter.return_value.first.return_value = mock_tenant - - mock_query2 = MagicMock() - mock_query2.join.return_value.where.return_value.first.return_value = None # No account found - - mock_db_dependencies["db"].session.query.side_effect = [mock_query1, mock_query2] + # Mock scalar for tenant, execute for account+role + mock_db_dependencies["db"].session.scalar.return_value = mock_tenant + mock_db_dependencies["db"].session.execute.return_value.first.return_value = None # No account found # Execute test result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") @@ -1901,14 +1821,9 @@ class TestRegisterService: } mock_redis_dependencies.get.return_value = json.dumps(invitation_data).encode() - # Mock database queries - mock_query1 = MagicMock() - mock_query1.filter.return_value.first.return_value = mock_tenant - - mock_query2 = MagicMock() - mock_query2.join.return_value.where.return_value.first.return_value = (mock_account, "normal") - - mock_db_dependencies["db"].session.query.side_effect = [mock_query1, mock_query2] + # Mock scalar for tenant, execute for account+role + mock_db_dependencies["db"].session.scalar.return_value = mock_tenant + mock_db_dependencies["db"].session.execute.return_value.first.return_value = (mock_account, "normal") # Execute test result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") diff --git a/api/tests/unit_tests/services/test_annotation_service.py b/api/tests/unit_tests/services/test_annotation_service.py index 0aacfc7f13..4295315f48 100644 --- a/api/tests/unit_tests/services/test_annotation_service.py +++ b/api/tests/unit_tests/services/test_annotation_service.py @@ -79,10 +79,7 @@ class TestAppAnnotationServiceUpInsert: patch("services.annotation_service.current_account_with_tenant", return_value=(current_user, tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = None - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(NotFound): @@ -100,10 +97,7 @@ class TestAppAnnotationServiceUpInsert: patch("services.annotation_service.current_account_with_tenant", return_value=(current_user, tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = app # Act & Assert with pytest.raises(ValueError): @@ -121,15 +115,7 @@ class TestAppAnnotationServiceUpInsert: patch("services.annotation_service.current_account_with_tenant", return_value=(current_user, tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - - message_query = MagicMock() - message_query.where.return_value = message_query - message_query.first.return_value = None - - mock_db.session.query.side_effect = [app_query, message_query] + mock_db.session.scalar.side_effect = [app, None] # Act & Assert with pytest.raises(NotFound): @@ -152,19 +138,7 @@ class TestAppAnnotationServiceUpInsert: patch("services.annotation_service.db") as mock_db, patch("services.annotation_service.add_annotation_to_index_task") as mock_task, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - - message_query = MagicMock() - message_query.where.return_value = message_query - message_query.first.return_value = message - - setting_query = MagicMock() - setting_query.where.return_value = setting_query - setting_query.first.return_value = setting - - mock_db.session.query.side_effect = [app_query, message_query, setting_query] + mock_db.session.scalar.side_effect = [app, message, setting] # Act result = AppAnnotationService.up_insert_app_annotation_from_message(args, app.id) @@ -202,19 +176,7 @@ class TestAppAnnotationServiceUpInsert: patch("services.annotation_service.MessageAnnotation", return_value=annotation_instance) as mock_cls, patch("services.annotation_service.add_annotation_to_index_task") as mock_task, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - - message_query = MagicMock() - message_query.where.return_value = message_query - message_query.first.return_value = message - - setting_query = MagicMock() - setting_query.where.return_value = setting_query - setting_query.first.return_value = None - - mock_db.session.query.side_effect = [app_query, message_query, setting_query] + mock_db.session.scalar.side_effect = [app, message, None] # Act result = AppAnnotationService.up_insert_app_annotation_from_message(args, app.id) @@ -245,10 +207,7 @@ class TestAppAnnotationServiceUpInsert: patch("services.annotation_service.current_account_with_tenant", return_value=(current_user, tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = app # Act & Assert with pytest.raises(ValueError): @@ -270,15 +229,7 @@ class TestAppAnnotationServiceUpInsert: patch("services.annotation_service.MessageAnnotation", return_value=annotation_instance) as mock_cls, patch("services.annotation_service.add_annotation_to_index_task") as mock_task, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - - setting_query = MagicMock() - setting_query.where.return_value = setting_query - setting_query.first.return_value = setting - - mock_db.session.query.side_effect = [app_query, setting_query] + mock_db.session.scalar.side_effect = [app, setting] # Act result = AppAnnotationService.up_insert_app_annotation_from_message(args, app.id) @@ -406,10 +357,7 @@ class TestAppAnnotationServiceListAndExport: patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = None - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(NotFound): @@ -427,10 +375,7 @@ class TestAppAnnotationServiceListAndExport: patch("services.annotation_service.db") as mock_db, patch("libs.helper.escape_like_pattern", return_value="safe"), ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = app mock_db.paginate.return_value = pagination # Act @@ -451,10 +396,7 @@ class TestAppAnnotationServiceListAndExport: patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = app mock_db.paginate.return_value = pagination # Act @@ -481,16 +423,8 @@ class TestAppAnnotationServiceListAndExport: patch("services.annotation_service.db") as mock_db, patch("services.annotation_service.CSVSanitizer.sanitize_value", side_effect=lambda v: f"safe:{v}"), ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - - annotation_query = MagicMock() - annotation_query.where.return_value = annotation_query - annotation_query.order_by.return_value = annotation_query - annotation_query.all.return_value = [annotation1, annotation2] - - mock_db.session.query.side_effect = [app_query, annotation_query] + mock_db.session.scalar.return_value = app + mock_db.session.scalars.return_value.all.return_value = [annotation1, annotation2] # Act result = AppAnnotationService.export_annotation_list_by_app_id(app.id) @@ -511,10 +445,7 @@ class TestAppAnnotationServiceListAndExport: patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = None - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(NotFound): @@ -534,10 +465,7 @@ class TestAppAnnotationServiceDirectManipulation: patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = None - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(NotFound): @@ -554,10 +482,7 @@ class TestAppAnnotationServiceDirectManipulation: patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = app # Act & Assert with pytest.raises(ValueError): @@ -579,15 +504,7 @@ class TestAppAnnotationServiceDirectManipulation: patch("services.annotation_service.MessageAnnotation", return_value=annotation_instance) as mock_cls, patch("services.annotation_service.add_annotation_to_index_task") as mock_task, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - - setting_query = MagicMock() - setting_query.where.return_value = setting_query - setting_query.first.return_value = setting - - mock_db.session.query.side_effect = [app_query, setting_query] + mock_db.session.scalar.side_effect = [app, setting] # Act result = AppAnnotationService.insert_app_annotation_directly(args, app.id) @@ -621,15 +538,8 @@ class TestAppAnnotationServiceDirectManipulation: patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - - annotation_query = MagicMock() - annotation_query.where.return_value = annotation_query - annotation_query.first.return_value = None - - mock_db.session.query.side_effect = [app_query, annotation_query] + mock_db.session.scalar.return_value = app + mock_db.session.get.return_value = None # Act & Assert with pytest.raises(NotFound): @@ -645,10 +555,7 @@ class TestAppAnnotationServiceDirectManipulation: patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = None - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(NotFound): @@ -666,15 +573,8 @@ class TestAppAnnotationServiceDirectManipulation: patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - - annotation_query = MagicMock() - annotation_query.where.return_value = annotation_query - annotation_query.first.return_value = annotation - - mock_db.session.query.side_effect = [app_query, annotation_query] + mock_db.session.scalar.return_value = app + mock_db.session.get.return_value = annotation # Act & Assert with pytest.raises(ValueError): @@ -695,19 +595,8 @@ class TestAppAnnotationServiceDirectManipulation: patch("services.annotation_service.db") as mock_db, patch("services.annotation_service.update_annotation_to_index_task") as mock_task, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - - annotation_query = MagicMock() - annotation_query.where.return_value = annotation_query - annotation_query.first.return_value = annotation - - setting_query = MagicMock() - setting_query.where.return_value = setting_query - setting_query.first.return_value = setting - - mock_db.session.query.side_effect = [app_query, annotation_query, setting_query] + mock_db.session.scalar.side_effect = [app, setting] + mock_db.session.get.return_value = annotation # Act result = AppAnnotationService.update_app_annotation_directly(args, app.id, annotation.id) @@ -740,22 +629,11 @@ class TestAppAnnotationServiceDirectManipulation: patch("services.annotation_service.db") as mock_db, patch("services.annotation_service.delete_annotation_index_task") as mock_task, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - - annotation_query = MagicMock() - annotation_query.where.return_value = annotation_query - annotation_query.first.return_value = annotation - - setting_query = MagicMock() - setting_query.where.return_value = setting_query - setting_query.first.return_value = setting + mock_db.session.scalar.side_effect = [app, setting] + mock_db.session.get.return_value = annotation scalars_result = MagicMock() scalars_result.all.return_value = [history1, history2] - - mock_db.session.query.side_effect = [app_query, annotation_query, setting_query] mock_db.session.scalars.return_value = scalars_result # Act @@ -782,10 +660,7 @@ class TestAppAnnotationServiceDirectManipulation: patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = None - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(NotFound): @@ -801,15 +676,8 @@ class TestAppAnnotationServiceDirectManipulation: patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - - annotation_query = MagicMock() - annotation_query.where.return_value = annotation_query - annotation_query.first.return_value = None - - mock_db.session.query.side_effect = [app_query, annotation_query] + mock_db.session.scalar.return_value = app + mock_db.session.get.return_value = None # Act & Assert with pytest.raises(NotFound): @@ -825,16 +693,8 @@ class TestAppAnnotationServiceDirectManipulation: patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - - annotations_query = MagicMock() - annotations_query.outerjoin.return_value = annotations_query - annotations_query.where.return_value = annotations_query - annotations_query.all.return_value = [] - - mock_db.session.query.side_effect = [app_query, annotations_query] + mock_db.session.scalar.return_value = app + mock_db.session.execute.return_value.all.return_value = [] # Act result = AppAnnotationService.delete_app_annotations_in_batch(app.id, ["ann-1"]) @@ -851,10 +711,7 @@ class TestAppAnnotationServiceDirectManipulation: patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = None - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(NotFound): @@ -874,24 +731,14 @@ class TestAppAnnotationServiceDirectManipulation: patch("services.annotation_service.db") as mock_db, patch("services.annotation_service.delete_annotation_index_task") as mock_task, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app + mock_db.session.scalar.return_value = app - annotations_query = MagicMock() - annotations_query.outerjoin.return_value = annotations_query - annotations_query.where.return_value = annotations_query - annotations_query.all.return_value = [(annotation1, setting), (annotation2, None)] - - hit_history_query = MagicMock() - hit_history_query.where.return_value = hit_history_query - hit_history_query.delete.return_value = None - - delete_query = MagicMock() - delete_query.where.return_value = delete_query - delete_query.delete.return_value = 2 - - mock_db.session.query.side_effect = [app_query, annotations_query, hit_history_query, delete_query] + # First execute().all() for multi-column query, subsequent execute() calls for deletes + execute_result_multi = MagicMock() + execute_result_multi.all.return_value = [(annotation1, setting), (annotation2, None)] + execute_result_delete = MagicMock() + execute_result_delete.rowcount = 2 + mock_db.session.execute.side_effect = [execute_result_multi, MagicMock(), execute_result_delete] # Act result = AppAnnotationService.delete_app_annotations_in_batch(app.id, ["ann-1", "ann-2"]) @@ -915,10 +762,7 @@ class TestAppAnnotationServiceBatchImport: patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = None - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(NotFound): @@ -941,10 +785,7 @@ class TestAppAnnotationServiceBatchImport: new=SimpleNamespace(ANNOTATION_IMPORT_MAX_RECORDS=5, ANNOTATION_IMPORT_MIN_RECORDS=1), ), ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = app # Act result = AppAnnotationService.batch_import_app_annotations(app.id, file) @@ -968,10 +809,7 @@ class TestAppAnnotationServiceBatchImport: new=SimpleNamespace(ANNOTATION_IMPORT_MAX_RECORDS=5, ANNOTATION_IMPORT_MIN_RECORDS=1), ), ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = app # Act result = AppAnnotationService.batch_import_app_annotations(app.id, file) @@ -999,10 +837,7 @@ class TestAppAnnotationServiceBatchImport: new=SimpleNamespace(ANNOTATION_IMPORT_MAX_RECORDS=5, ANNOTATION_IMPORT_MIN_RECORDS=2), ), ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = app # Act result = AppAnnotationService.batch_import_app_annotations(app.id, file) @@ -1028,10 +863,7 @@ class TestAppAnnotationServiceBatchImport: new=SimpleNamespace(ANNOTATION_IMPORT_MAX_RECORDS=1, ANNOTATION_IMPORT_MIN_RECORDS=1), ), ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = app # Act result = AppAnnotationService.batch_import_app_annotations(app.id, file) @@ -1061,10 +893,7 @@ class TestAppAnnotationServiceBatchImport: new=SimpleNamespace(ANNOTATION_IMPORT_MAX_RECORDS=5, ANNOTATION_IMPORT_MIN_RECORDS=1), ), ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = app # Act result = AppAnnotationService.batch_import_app_annotations(app.id, file) @@ -1090,10 +919,7 @@ class TestAppAnnotationServiceBatchImport: new=SimpleNamespace(ANNOTATION_IMPORT_MAX_RECORDS=5, ANNOTATION_IMPORT_MIN_RECORDS=1), ), ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = app # Act result = AppAnnotationService.batch_import_app_annotations(app.id, file) @@ -1119,10 +945,7 @@ class TestAppAnnotationServiceBatchImport: new=SimpleNamespace(ANNOTATION_IMPORT_MAX_RECORDS=5, ANNOTATION_IMPORT_MIN_RECORDS=1), ), ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = app # Act result = AppAnnotationService.batch_import_app_annotations(app.id, file) @@ -1148,10 +971,7 @@ class TestAppAnnotationServiceBatchImport: new=SimpleNamespace(ANNOTATION_IMPORT_MAX_RECORDS=5, ANNOTATION_IMPORT_MIN_RECORDS=1), ), ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = app # Act result = AppAnnotationService.batch_import_app_annotations(app.id, file) @@ -1182,10 +1002,7 @@ class TestAppAnnotationServiceBatchImport: new=SimpleNamespace(ANNOTATION_IMPORT_MAX_RECORDS=5, ANNOTATION_IMPORT_MIN_RECORDS=1), ), ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = app # Act result = AppAnnotationService.batch_import_app_annotations(app.id, file) @@ -1218,10 +1035,7 @@ class TestAppAnnotationServiceBatchImport: new=SimpleNamespace(ANNOTATION_IMPORT_MAX_RECORDS=5, ANNOTATION_IMPORT_MIN_RECORDS=1), ), ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = app # Act result = AppAnnotationService.batch_import_app_annotations(app.id, file) @@ -1257,10 +1071,7 @@ class TestAppAnnotationServiceBatchImport: new=SimpleNamespace(ANNOTATION_IMPORT_MAX_RECORDS=5, ANNOTATION_IMPORT_MIN_RECORDS=1), ), ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = app mock_redis.zadd.side_effect = RuntimeError("boom") mock_redis.zrem.side_effect = RuntimeError("cleanup-failed") @@ -1285,10 +1096,7 @@ class TestAppAnnotationServiceHitHistoryAndSettings: patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = None - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(NotFound): @@ -1306,15 +1114,8 @@ class TestAppAnnotationServiceHitHistoryAndSettings: patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - - annotation_query = MagicMock() - annotation_query.where.return_value = annotation_query - annotation_query.first.return_value = annotation - - mock_db.session.query.side_effect = [app_query, annotation_query] + mock_db.session.scalar.return_value = app + mock_db.session.get.return_value = annotation mock_db.paginate.return_value = pagination # Act @@ -1334,15 +1135,8 @@ class TestAppAnnotationServiceHitHistoryAndSettings: patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - - annotation_query = MagicMock() - annotation_query.where.return_value = annotation_query - annotation_query.first.return_value = None - - mock_db.session.query.side_effect = [app_query, annotation_query] + mock_db.session.scalar.return_value = app + mock_db.session.get.return_value = None # Act & Assert with pytest.raises(NotFound): @@ -1352,10 +1146,7 @@ class TestAppAnnotationServiceHitHistoryAndSettings: """Test get_annotation_by_id returns None when not found.""" # Arrange with patch("services.annotation_service.db") as mock_db: - query = MagicMock() - query.where.return_value = query - query.first.return_value = None - mock_db.session.query.return_value = query + mock_db.session.get.return_value = None # Act result = AppAnnotationService.get_annotation_by_id("ann-1") @@ -1368,10 +1159,7 @@ class TestAppAnnotationServiceHitHistoryAndSettings: # Arrange annotation = _make_annotation("ann-1") with patch("services.annotation_service.db") as mock_db: - query = MagicMock() - query.where.return_value = query - query.first.return_value = annotation - mock_db.session.query.return_value = query + mock_db.session.get.return_value = annotation # Act result = AppAnnotationService.get_annotation_by_id("ann-1") @@ -1386,10 +1174,6 @@ class TestAppAnnotationServiceHitHistoryAndSettings: patch("services.annotation_service.db") as mock_db, patch("services.annotation_service.AppAnnotationHitHistory") as mock_history_cls, ): - query = MagicMock() - query.where.return_value = query - mock_db.session.query.return_value = query - # Act AppAnnotationService.add_annotation_history( annotation_id="ann-1", @@ -1404,7 +1188,7 @@ class TestAppAnnotationServiceHitHistoryAndSettings: ) # Assert - query.update.assert_called_once() + mock_db.session.execute.assert_called_once() mock_history_cls.assert_called_once() mock_db.session.add.assert_called_once() mock_db.session.commit.assert_called_once() @@ -1420,15 +1204,7 @@ class TestAppAnnotationServiceHitHistoryAndSettings: patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - - setting_query = MagicMock() - setting_query.where.return_value = setting_query - setting_query.first.return_value = setting - - mock_db.session.query.side_effect = [app_query, setting_query] + mock_db.session.scalar.side_effect = [app, setting] # Act result = AppAnnotationService.get_app_annotation_setting_by_app_id(app.id) @@ -1448,10 +1224,7 @@ class TestAppAnnotationServiceHitHistoryAndSettings: patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = None - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(NotFound): @@ -1468,15 +1241,7 @@ class TestAppAnnotationServiceHitHistoryAndSettings: patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - - setting_query = MagicMock() - setting_query.where.return_value = setting_query - setting_query.first.return_value = setting - - mock_db.session.query.side_effect = [app_query, setting_query] + mock_db.session.scalar.side_effect = [app, setting] # Act result = AppAnnotationService.get_app_annotation_setting_by_app_id(app.id) @@ -1495,15 +1260,7 @@ class TestAppAnnotationServiceHitHistoryAndSettings: patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - - setting_query = MagicMock() - setting_query.where.return_value = setting_query - setting_query.first.return_value = None - - mock_db.session.query.side_effect = [app_query, setting_query] + mock_db.session.scalar.side_effect = [app, None] # Act result = AppAnnotationService.get_app_annotation_setting_by_app_id(app.id) @@ -1525,15 +1282,7 @@ class TestAppAnnotationServiceHitHistoryAndSettings: patch("services.annotation_service.db") as mock_db, patch("services.annotation_service.naive_utc_now", return_value="now"), ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - - setting_query = MagicMock() - setting_query.where.return_value = setting_query - setting_query.first.return_value = setting - - mock_db.session.query.side_effect = [app_query, setting_query] + mock_db.session.scalar.side_effect = [app, setting] # Act result = AppAnnotationService.update_app_annotation_setting(app.id, setting.id, args) @@ -1560,15 +1309,7 @@ class TestAppAnnotationServiceHitHistoryAndSettings: patch("services.annotation_service.db") as mock_db, patch("services.annotation_service.naive_utc_now", return_value="now"), ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - - setting_query = MagicMock() - setting_query.where.return_value = setting_query - setting_query.first.return_value = setting - - mock_db.session.query.side_effect = [app_query, setting_query] + mock_db.session.scalar.side_effect = [app, setting] # Act result = AppAnnotationService.update_app_annotation_setting(app.id, setting.id, args) @@ -1587,10 +1328,7 @@ class TestAppAnnotationServiceHitHistoryAndSettings: patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = None - mock_db.session.query.return_value = app_query + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(NotFound): @@ -1606,15 +1344,7 @@ class TestAppAnnotationServiceHitHistoryAndSettings: patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, ): - app_query = MagicMock() - app_query.where.return_value = app_query - app_query.first.return_value = app - - setting_query = MagicMock() - setting_query.where.return_value = setting_query - setting_query.first.return_value = None - - mock_db.session.query.side_effect = [app_query, setting_query] + mock_db.session.scalar.side_effect = [app, None] # Act & Assert with pytest.raises(NotFound): @@ -1634,25 +1364,21 @@ class TestAppAnnotationServiceClearAll: annotation2 = _make_annotation("ann-2") history = MagicMock(spec=AppAnnotationHitHistory) - def query_side_effect(*args: object, **kwargs: object) -> MagicMock: - query = MagicMock() - query.where.return_value = query - if App in args: - query.first.return_value = app - elif AppAnnotationSetting in args: - query.first.return_value = setting - elif MessageAnnotation in args: - query.yield_per.return_value = [annotation1, annotation2] - elif AppAnnotationHitHistory in args: - query.yield_per.return_value = [history] - return query - with ( patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, patch("services.annotation_service.delete_annotation_index_task") as mock_task, ): - mock_db.session.query.side_effect = query_side_effect + # scalar calls: app lookup, annotation_setting lookup + mock_db.session.scalar.side_effect = [app, setting] + # scalars calls: first for annotations iteration, then for each annotation's hit histories + annotations_scalars = MagicMock() + annotations_scalars.yield_per.return_value = [annotation1, annotation2] + histories_scalars_1 = MagicMock() + histories_scalars_1.yield_per.return_value = [history] + histories_scalars_2 = MagicMock() + histories_scalars_2.yield_per.return_value = [] + mock_db.session.scalars.side_effect = [annotations_scalars, histories_scalars_1, histories_scalars_2] # Act result = AppAnnotationService.clear_all_annotations(app.id) @@ -1675,10 +1401,7 @@ class TestAppAnnotationServiceClearAll: patch("services.annotation_service.current_account_with_tenant", return_value=(_make_user(), tenant_id)), patch("services.annotation_service.db") as mock_db, ): - query = MagicMock() - query.where.return_value = query - query.first.return_value = None - mock_db.session.query.return_value = query + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(NotFound): diff --git a/api/tests/unit_tests/services/test_app_dsl_service.py b/api/tests/unit_tests/services/test_app_dsl_service.py index 179518a5fa..b2a2a1f685 100644 --- a/api/tests/unit_tests/services/test_app_dsl_service.py +++ b/api/tests/unit_tests/services/test_app_dsl_service.py @@ -11,7 +11,7 @@ from core.trigger.constants import ( TRIGGER_SCHEDULE_NODE_TYPE, TRIGGER_WEBHOOK_NODE_TYPE, ) -from models import Account, AppMode +from models import Account, App, AppMode from models.model import IconType from services import app_dsl_service from services.app_dsl_service import ( @@ -41,6 +41,14 @@ def _account_mock(*, tenant_id: str = "tenant-1", account_id: str = "account-1") return account +def _app_mock(**kwargs: object) -> MagicMock: + """Create a MagicMock with spec=App for type-safe test doubles.""" + app = MagicMock(spec=App) + for key, value in kwargs.items(): + setattr(app, key, value) + return app + + def _yaml_dump(data: dict) -> str: return yaml.safe_dump(data, allow_unicode=True) @@ -194,7 +202,7 @@ def test_import_app_overwrite_only_allows_workflow_and_advanced_chat(monkeypatch monkeypatch.setattr(app_dsl_service, "select", fake_select) - existing_app = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.CHAT.value) + existing_app = _app_mock(id="app-1", tenant_id="tenant-1", mode=AppMode.CHAT.value) session = MagicMock() session.scalar.return_value = existing_app @@ -241,7 +249,7 @@ def test_import_app_completed_uses_declared_dependencies(monkeypatch): lambda d: plugin_deps[0], ) - created_app = SimpleNamespace(id="app-new", mode=AppMode.WORKFLOW.value, tenant_id="tenant-1") + created_app = _app_mock(id="app-new", mode=AppMode.WORKFLOW.value, tenant_id="tenant-1") monkeypatch.setattr(AppDslService, "_create_or_update_app", lambda *_args, **_kwargs: created_app) draft_var_service = MagicMock() @@ -285,7 +293,7 @@ def test_import_app_legacy_versions_extract_dependencies(monkeypatch, has_workfl lambda deps: [SimpleNamespace(model_dump=lambda: {"dep": deps[0]})], ) - created_app = SimpleNamespace(id="app-legacy", mode=AppMode.WORKFLOW.value, tenant_id="tenant-1") + created_app = _app_mock(id="app-legacy", mode=AppMode.WORKFLOW.value, tenant_id="tenant-1") monkeypatch.setattr(AppDslService, "_create_or_update_app", lambda *_args, **_kwargs: created_app) draft_var_service = MagicMock() @@ -373,7 +381,7 @@ def test_confirm_import_success_deletes_redis_key(monkeypatch): ) app_dsl_service.redis_client.get.return_value = pending.model_dump_json() - created_app = SimpleNamespace(id="confirmed-app", mode=AppMode.WORKFLOW.value, tenant_id="tenant-1") + created_app = _app_mock(id="confirmed-app", mode=AppMode.WORKFLOW.value, tenant_id="tenant-1") monkeypatch.setattr(AppDslService, "_create_or_update_app", lambda *_args, **_kwargs: created_app) app_dsl_service.redis_client.delete.reset_mock() @@ -399,7 +407,7 @@ def test_confirm_import_exception_returns_failed(monkeypatch): def test_check_dependencies_returns_empty_when_no_redis_data(): service = AppDslService(MagicMock()) - result = service.check_dependencies(app_model=SimpleNamespace(id="app-1", tenant_id="tenant-1")) + result = service.check_dependencies(app_model=_app_mock(id="app-1", tenant_id="tenant-1")) assert result.leaked_dependencies == [] @@ -416,7 +424,7 @@ def test_check_dependencies_calls_analysis_service(monkeypatch): ) service = AppDslService(MagicMock()) - result = service.check_dependencies(app_model=SimpleNamespace(id="app-1", tenant_id="tenant-1")) + result = service.check_dependencies(app_model=_app_mock(id="app-1", tenant_id="tenant-1")) assert len(result.leaked_dependencies) == 1 @@ -444,7 +452,7 @@ def test_create_or_update_app_existing_app_updates_fields(monkeypatch): lambda _m: SimpleNamespace(kind="conv"), ) - app = SimpleNamespace( + app = _app_mock( id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW.value, @@ -554,7 +562,7 @@ def test_create_or_update_app_workflow_missing_workflow_data_raises(): service = AppDslService(MagicMock()) with pytest.raises(ValueError, match="Missing workflow data"): service._create_or_update_app( - app=SimpleNamespace( + app=_app_mock( id="a", tenant_id="t", mode=AppMode.WORKFLOW.value, @@ -572,7 +580,7 @@ def test_create_or_update_app_chat_requires_model_config(): service = AppDslService(MagicMock()) with pytest.raises(ValueError, match="Missing model_config"): service._create_or_update_app( - app=SimpleNamespace( + app=_app_mock( id="a", tenant_id="t", mode=AppMode.CHAT.value, @@ -601,7 +609,7 @@ def test_create_or_update_app_chat_creates_model_config_and_sends_event(monkeypa session = MagicMock() service = AppDslService(session) - app = SimpleNamespace( + app = _app_mock( id="app-1", tenant_id="tenant-1", mode=AppMode.CHAT.value, @@ -625,7 +633,7 @@ def test_create_or_update_app_invalid_mode_raises(): service = AppDslService(MagicMock()) with pytest.raises(ValueError, match="Invalid app mode"): service._create_or_update_app( - app=SimpleNamespace( + app=_app_mock( id="a", tenant_id="t", mode=AppMode.RAG_PIPELINE.value, @@ -647,7 +655,7 @@ def test_export_dsl_delegates_by_mode(monkeypatch): AppDslService, "_append_model_config_export_data", lambda *_args, **_kwargs: model_calls.append(True) ) - workflow_app = SimpleNamespace( + workflow_app = _app_mock( mode=AppMode.WORKFLOW.value, tenant_id="tenant-1", name="n", @@ -661,7 +669,7 @@ def test_export_dsl_delegates_by_mode(monkeypatch): AppDslService.export_dsl(workflow_app) assert workflow_calls == [True] - chat_app = SimpleNamespace( + chat_app = _app_mock( mode=AppMode.CHAT.value, tenant_id="tenant-1", name="n", @@ -679,7 +687,7 @@ def test_export_dsl_delegates_by_mode(monkeypatch): def test_export_dsl_preserves_icon_and_icon_type(monkeypatch): monkeypatch.setattr(AppDslService, "_append_workflow_export_data", lambda **_kwargs: None) - emoji_app = SimpleNamespace( + emoji_app = _app_mock( mode=AppMode.WORKFLOW.value, tenant_id="tenant-1", name="Emoji App", @@ -696,7 +704,7 @@ def test_export_dsl_preserves_icon_and_icon_type(monkeypatch): assert data["app"]["icon_type"] == "emoji" assert data["app"]["icon_background"] == "#FF5733" - image_app = SimpleNamespace( + image_app = _app_mock( mode=AppMode.WORKFLOW.value, tenant_id="tenant-1", name="Image App", @@ -759,7 +767,7 @@ def test_append_workflow_export_data_filters_and_overrides(monkeypatch): export_data: dict = {} AppDslService._append_workflow_export_data( export_data=export_data, - app_model=SimpleNamespace(tenant_id="tenant-1"), + app_model=_app_mock(tenant_id="tenant-1"), include_secret=False, workflow_id=None, ) @@ -783,7 +791,7 @@ def test_append_workflow_export_data_missing_workflow_raises(monkeypatch): with pytest.raises(ValueError, match="Missing draft workflow configuration"): AppDslService._append_workflow_export_data( export_data={}, - app_model=SimpleNamespace(tenant_id="tenant-1"), + app_model=_app_mock(tenant_id="tenant-1"), include_secret=False, workflow_id=None, ) @@ -801,7 +809,7 @@ def test_append_model_config_export_data_filters_credential_id(monkeypatch): monkeypatch.setattr(app_dsl_service, "jsonable_encoder", lambda x: x) app_model_config = SimpleNamespace(to_dict=lambda: {"agent_mode": {"tools": [{"credential_id": "secret"}]}}) - app_model = SimpleNamespace(tenant_id="tenant-1", app_model_config=app_model_config) + app_model = _app_mock(tenant_id="tenant-1", app_model_config=app_model_config) export_data: dict = {} AppDslService._append_model_config_export_data(export_data, app_model) @@ -811,7 +819,7 @@ def test_append_model_config_export_data_filters_credential_id(monkeypatch): def test_append_model_config_export_data_requires_app_config(): with pytest.raises(ValueError, match="Missing app configuration"): - AppDslService._append_model_config_export_data({}, SimpleNamespace(app_model_config=None)) + AppDslService._append_model_config_export_data({}, _app_mock(app_model_config=None)) def test_extract_dependencies_from_workflow_graph_covers_all_node_types(monkeypatch): diff --git a/api/tests/unit_tests/services/test_async_workflow_service.py b/api/tests/unit_tests/services/test_async_workflow_service.py index 07f8324d13..361e95a557 100644 --- a/api/tests/unit_tests/services/test_async_workflow_service.py +++ b/api/tests/unit_tests/services/test_async_workflow_service.py @@ -361,11 +361,12 @@ class TestAsyncWorkflowService: mock_session_context.__enter__.return_value = mock_session mock_session_context.__exit__.return_value = None + mock_sessionmaker = MagicMock() + mock_sessionmaker.return_value.begin.return_value = mock_session_context + with ( patch.object(async_workflow_service_module, "db", new=SimpleNamespace(engine=fake_engine)), - patch.object( - async_workflow_service_module, "Session", return_value=mock_session_context - ) as mock_session_class, + patch.object(async_workflow_service_module, "sessionmaker", mock_sessionmaker), patch.object( async_workflow_service_module, "SQLAlchemyWorkflowTriggerLogRepository", @@ -377,7 +378,7 @@ class TestAsyncWorkflowService: # Assert assert result == expected - mock_session_class.assert_called_once_with(fake_engine) + mock_sessionmaker.assert_called_once_with(fake_engine) mock_repo.get_by_id.assert_called_once_with("trigger-log-123", "tenant-123") def test_should_return_recent_logs_as_dict_list(self): @@ -395,9 +396,12 @@ class TestAsyncWorkflowService: mock_session_context.__enter__.return_value = mock_session mock_session_context.__exit__.return_value = None + mock_sessionmaker = MagicMock() + mock_sessionmaker.return_value.begin.return_value = mock_session_context + with ( patch.object(async_workflow_service_module, "db", new=SimpleNamespace(engine=MagicMock())), - patch.object(async_workflow_service_module, "Session", return_value=mock_session_context), + patch.object(async_workflow_service_module, "sessionmaker", mock_sessionmaker), patch.object( async_workflow_service_module, "SQLAlchemyWorkflowTriggerLogRepository", @@ -436,9 +440,12 @@ class TestAsyncWorkflowService: mock_session_context.__enter__.return_value = mock_session mock_session_context.__exit__.return_value = None + mock_sessionmaker = MagicMock() + mock_sessionmaker.return_value.begin.return_value = mock_session_context + with ( patch.object(async_workflow_service_module, "db", new=SimpleNamespace(engine=MagicMock())), - patch.object(async_workflow_service_module, "Session", return_value=mock_session_context), + patch.object(async_workflow_service_module, "sessionmaker", mock_sessionmaker), patch.object( async_workflow_service_module, "SQLAlchemyWorkflowTriggerLogRepository", diff --git a/api/tests/unit_tests/services/test_billing_service.py b/api/tests/unit_tests/services/test_billing_service.py index 2d3700f50e..34f718ba02 100644 --- a/api/tests/unit_tests/services/test_billing_service.py +++ b/api/tests/unit_tests/services/test_billing_service.py @@ -640,6 +640,38 @@ class TestBillingServiceQuotaOperations: assert result["released"] == 1 assert isinstance(result["released"], int) + def test_get_quota_info_coerces_string_to_int(self, mock_send_request): + """Test that TypeAdapter coerces string values to int for get_quota_info.""" + mock_send_request.return_value = { + "trigger_event": {"usage": "42", "limit": "3000", "reset_date": "1700000000"}, + "api_rate_limit": {"usage": "10", "limit": "-1", "reset_date": "-1"}, + } + + result = BillingService.get_quota_info("t1") + + assert result["trigger_event"]["usage"] == 42 + assert isinstance(result["trigger_event"]["usage"], int) + assert result["trigger_event"]["limit"] == 3000 + assert isinstance(result["trigger_event"]["limit"], int) + assert result["trigger_event"]["reset_date"] == 1700000000 + assert isinstance(result["trigger_event"]["reset_date"], int) + assert result["api_rate_limit"]["limit"] == -1 + assert isinstance(result["api_rate_limit"]["limit"], int) + + def test_get_quota_info_accepts_int_values(self, mock_send_request): + """Test that get_quota_info works with native int values.""" + expected = { + "trigger_event": {"usage": 42, "limit": 3000, "reset_date": 1700000000}, + "api_rate_limit": {"usage": 0, "limit": -1}, + } + mock_send_request.return_value = expected + + result = BillingService.get_quota_info("t1") + + assert result["trigger_event"]["usage"] == 42 + assert result["trigger_event"]["limit"] == 3000 + assert result["api_rate_limit"]["limit"] == -1 + class TestBillingServiceRateLimitEnforcement: """Unit tests for rate limit enforcement mechanisms. @@ -1724,7 +1756,7 @@ class TestBillingServiceSubscriptionInfoDataType: }, "members": {"size": "10", "limit": "50"}, "apps": {"size": "80", "limit": "200"}, - "vector_space": {"size": "5120.75", "limit": "20480"}, + "vector_space": {"size": 5120.75, "limit": "20480"}, "knowledge_rate_limit": {"limit": "1000"}, "documents_upload_quota": {"size": "450", "limit": "1000"}, "annotation_quota_limit": {"size": "1200", "limit": "5000"}, diff --git a/api/tests/unit_tests/services/test_clear_free_plan_tenant_expired_logs.py b/api/tests/unit_tests/services/test_clear_free_plan_tenant_expired_logs.py index 1926cb133a..f393a4b10b 100644 --- a/api/tests/unit_tests/services/test_clear_free_plan_tenant_expired_logs.py +++ b/api/tests/unit_tests/services/test_clear_free_plan_tenant_expired_logs.py @@ -209,8 +209,22 @@ def _session_wrapper_for_no_autoflush(session: Mock) -> Mock: return wrapper +def _sessionmaker_wrapper_for_begin(session: Mock) -> Mock: + """ + ClearFreePlanTenantExpiredLogs.process uses: with sessionmaker(db.engine).begin() as session: + so sessionmaker(db.engine) must return an object with a begin() method that returns a context manager. + """ + begin_cm = MagicMock() + begin_cm.__enter__.return_value = session + begin_cm.__exit__.return_value = None + + sessionmaker_result = MagicMock() + sessionmaker_result.begin.return_value = begin_cm + return sessionmaker_result + + def _session_wrapper_for_direct(session: Mock) -> Mock: - """ClearFreePlanTenantExpiredLogs.process uses: with Session(db.engine) as session:""" + """ClearFreePlanTenantExpiredLogs.process uses: with Session(db.engine) as session: (for old code paths)""" wrapper = MagicMock() wrapper.__enter__.return_value = session wrapper.__exit__.return_value = None @@ -348,7 +362,7 @@ def test_process_with_tenant_ids_filters_by_plan_and_logs_errors(monkeypatch: py count_query.count.return_value = 2 count_session.query.return_value = count_query - monkeypatch.setattr(service_module, "Session", lambda _engine: _session_wrapper_for_direct(count_session)) + monkeypatch.setattr(service_module, "sessionmaker", lambda _engine: _sessionmaker_wrapper_for_begin(count_session)) # Avoid LocalProxy usage flask_app = service_module.Flask("test-app") @@ -438,8 +452,8 @@ def test_process_without_tenant_ids_batches_and_scales_interval(monkeypatch: pyt batch_session.query.side_effect = [q1, q2, q3, q4, q_rs] - sessions = [_session_wrapper_for_direct(total_session), _session_wrapper_for_direct(batch_session)] - monkeypatch.setattr(service_module, "Session", lambda _engine: sessions.pop(0)) + sessions = [_sessionmaker_wrapper_for_begin(total_session), _sessionmaker_wrapper_for_begin(batch_session)] + monkeypatch.setattr(service_module, "sessionmaker", lambda _engine: sessions.pop(0)) process_tenant_mock = MagicMock() monkeypatch.setattr(ClearFreePlanTenantExpiredLogs, "process_tenant", process_tenant_mock) @@ -457,7 +471,7 @@ def test_process_with_tenant_ids_emits_progress_every_100(monkeypatch: pytest.Mo count_query = MagicMock() count_query.count.return_value = 100 count_session.query.return_value = count_query - monkeypatch.setattr(service_module, "Session", lambda _engine: _session_wrapper_for_direct(count_session)) + monkeypatch.setattr(service_module, "sessionmaker", lambda _engine: _sessionmaker_wrapper_for_begin(count_session)) flask_app = service_module.Flask("test-app") monkeypatch.setattr(service_module, "current_app", SimpleNamespace(_get_current_object=lambda: flask_app)) @@ -523,8 +537,8 @@ def test_process_without_tenant_ids_all_intervals_too_many_uses_min_interval(mon batch_session.query.side_effect = [*count_queries, q_rs] - sessions = [_session_wrapper_for_direct(total_session), _session_wrapper_for_direct(batch_session)] - monkeypatch.setattr(service_module, "Session", lambda _engine: sessions.pop(0)) + sessions = [_sessionmaker_wrapper_for_begin(total_session), _sessionmaker_wrapper_for_begin(batch_session)] + monkeypatch.setattr(service_module, "sessionmaker", lambda _engine: sessions.pop(0)) process_tenant_mock = MagicMock() monkeypatch.setattr(ClearFreePlanTenantExpiredLogs, "process_tenant", process_tenant_mock) diff --git a/api/tests/unit_tests/services/test_dataset_service_dataset.py b/api/tests/unit_tests/services/test_dataset_service_dataset.py index 92aed7c30a..b2c40763ea 100644 --- a/api/tests/unit_tests/services/test_dataset_service_dataset.py +++ b/api/tests/unit_tests/services/test_dataset_service_dataset.py @@ -62,7 +62,7 @@ class TestDatasetServiceQueries: self, mock_dataset_query_dependencies ): user = DatasetServiceUnitDataFactory.create_user_mock(role=TenantAccountRole.DATASET_OPERATOR) - mock_dataset_query_dependencies["db"].session.query.return_value.filter_by.return_value.all.return_value = [] + mock_dataset_query_dependencies["db"].session.scalars.return_value.all.return_value = [] items, total = DatasetService.get_datasets(page=1, per_page=20, tenant_id="tenant-1", user=user) @@ -108,9 +108,7 @@ class TestDatasetServiceQueries: dataset_process_rule.rules_dict = {"delimiter": "\n"} with patch("services.dataset_service.db") as mock_db: - ( - mock_db.session.query.return_value.where.return_value.order_by.return_value.limit.return_value.one_or_none.return_value - ) = dataset_process_rule + (mock_db.session.execute.return_value.scalar_one_or_none.return_value) = dataset_process_rule result = DatasetService.get_process_rules("dataset-1") @@ -118,9 +116,7 @@ class TestDatasetServiceQueries: def test_get_process_rules_falls_back_to_default_rules_when_missing(self): with patch("services.dataset_service.db") as mock_db: - ( - mock_db.session.query.return_value.where.return_value.order_by.return_value.limit.return_value.one_or_none.return_value - ) = None + (mock_db.session.execute.return_value.scalar_one_or_none.return_value) = None result = DatasetService.get_process_rules("dataset-1") @@ -151,7 +147,7 @@ class TestDatasetServiceQueries: dataset = DatasetServiceUnitDataFactory.create_dataset_mock() with patch("services.dataset_service.db") as mock_db: - mock_db.session.query.return_value.filter_by.return_value.first.return_value = dataset + mock_db.session.get.return_value = dataset result = DatasetService.get_dataset(dataset.id) @@ -308,7 +304,7 @@ class TestDatasetServiceCreationAndUpdate: account = SimpleNamespace(id="user-1") with patch("services.dataset_service.db") as mock_db: - mock_db.session.query.return_value.filter_by.return_value.first.return_value = object() + mock_db.session.scalar.return_value = object() with pytest.raises(DatasetNameDuplicateError, match="Dataset with name Dataset already exists"): DatasetService.create_empty_dataset("tenant-1", "Dataset", None, "economy", account) @@ -319,6 +315,7 @@ class TestDatasetServiceCreationAndUpdate: with ( patch("services.dataset_service.db") as mock_db, + patch("services.dataset_service.select"), patch( "services.dataset_service.Dataset", side_effect=lambda **kwargs: SimpleNamespace(id="dataset-1", **kwargs), @@ -326,7 +323,7 @@ class TestDatasetServiceCreationAndUpdate: patch("services.dataset_service.ModelManager") as model_manager_cls, patch.object(DatasetService, "check_embedding_model_setting") as check_embedding, ): - mock_db.session.query.return_value.filter_by.return_value.first.return_value = None + mock_db.session.scalar.return_value = None model_manager_cls.for_tenant.return_value.get_default_model_instance.return_value = default_embedding_model dataset = DatasetService.create_empty_dataset( @@ -355,6 +352,7 @@ class TestDatasetServiceCreationAndUpdate: with ( patch("services.dataset_service.db") as mock_db, + patch("services.dataset_service.select"), patch( "services.dataset_service.Dataset", side_effect=lambda **kwargs: SimpleNamespace(id="dataset-1", **kwargs), @@ -368,7 +366,7 @@ class TestDatasetServiceCreationAndUpdate: patch.object(DatasetService, "check_embedding_model_setting") as check_embedding, patch.object(DatasetService, "check_reranking_model_setting") as check_reranking, ): - mock_db.session.query.return_value.filter_by.return_value.first.return_value = None + mock_db.session.scalar.return_value = None model_manager_cls.for_tenant.return_value.get_model_instance.return_value = embedding_model dataset = DatasetService.create_empty_dataset( @@ -412,7 +410,7 @@ class TestDatasetServiceCreationAndUpdate: ) with patch("services.dataset_service.db") as mock_db: - mock_db.session.query.return_value.filter_by.return_value.first.return_value = object() + mock_db.session.scalar.return_value = object() with pytest.raises(DatasetNameDuplicateError, match="Existing Dataset already exists"): DatasetService.create_empty_rag_pipeline_dataset("tenant-1", entity) @@ -435,12 +433,13 @@ class TestDatasetServiceCreationAndUpdate: with ( patch("services.dataset_service.db") as mock_db, + patch("services.dataset_service.select"), patch("services.dataset_service.current_user", SimpleNamespace(id="user-1")), patch("services.dataset_service.generate_incremental_name", return_value="Untitled 2") as generate_name, patch("services.dataset_service.Pipeline", side_effect=pipeline_factory), patch("services.dataset_service.Dataset", side_effect=dataset_factory), ): - mock_db.session.query.return_value.filter_by.return_value.all.return_value = [ + mock_db.session.scalars.return_value.all.return_value = [ SimpleNamespace(name="Untitled"), SimpleNamespace(name="Untitled 1"), ] @@ -465,7 +464,7 @@ class TestDatasetServiceCreationAndUpdate: patch("services.dataset_service.db") as mock_db, patch("services.dataset_service.current_user", SimpleNamespace(id=None)), ): - mock_db.session.query.return_value.filter_by.return_value.first.return_value = None + mock_db.session.scalar.return_value = None with pytest.raises(ValueError, match="Current user or current user id not found"): DatasetService.create_empty_rag_pipeline_dataset("tenant-1", entity) @@ -520,7 +519,7 @@ class TestDatasetServiceCreationAndUpdate: def test_has_dataset_same_name_returns_true_when_query_matches(self): with patch("services.dataset_service.db") as mock_db: - mock_db.session.query.return_value.where.return_value.first.return_value = object() + mock_db.session.scalar.return_value = object() result = DatasetService._has_dataset_same_name("tenant-1", "dataset-1", "Dataset") @@ -579,26 +578,33 @@ class TestDatasetServiceCreationAndUpdate: binding = SimpleNamespace(external_knowledge_id="old-knowledge", external_knowledge_api_id="old-api") session = MagicMock() session.query.return_value.filter_by.return_value.first.return_value = binding + session.add = MagicMock() session_context = _make_session_context(session) + mock_sessionmaker = MagicMock() + mock_sessionmaker.return_value.begin.return_value = session_context + with ( patch("services.dataset_service.db") as mock_db, - patch("services.dataset_service.Session", return_value=session_context), + patch("services.dataset_service.sessionmaker", mock_sessionmaker), ): DatasetService._update_external_knowledge_binding("dataset-1", "new-knowledge", "new-api") assert binding.external_knowledge_id == "new-knowledge" assert binding.external_knowledge_api_id == "new-api" - mock_db.session.add.assert_called_once_with(binding) + session.add.assert_called_once_with(binding) def test_update_external_knowledge_binding_raises_for_missing_binding(self): session = MagicMock() session.query.return_value.filter_by.return_value.first.return_value = None session_context = _make_session_context(session) + mock_sessionmaker = MagicMock() + mock_sessionmaker.return_value.begin.return_value = session_context + with ( patch("services.dataset_service.db"), - patch("services.dataset_service.Session", return_value=session_context), + patch("services.dataset_service.sessionmaker", mock_sessionmaker), ): with pytest.raises(ValueError, match="External knowledge binding not found"): DatasetService._update_external_knowledge_binding("dataset-1", "knowledge-1", "api-1") @@ -630,7 +636,7 @@ class TestDatasetServiceCreationAndUpdate: result = DatasetService._update_internal_dataset(dataset, update_payload.copy(), user) assert result is dataset - updated_values = mock_db.session.query.return_value.filter_by.return_value.update.call_args.args[0] + updated_values = mock_db.session.execute.call_args.args[0].compile().params assert updated_values["name"] == "Updated Dataset" assert updated_values["description"] is None assert updated_values["retrieval_model"] == {"top_k": 4} @@ -658,13 +664,13 @@ class TestDatasetServiceCreationAndUpdate: with patch("services.dataset_service.db") as mock_db: DatasetService._update_pipeline_knowledge_base_node_data(dataset, "user-1") - mock_db.session.query.assert_not_called() + mock_db.session.get.assert_not_called() def test_update_pipeline_knowledge_base_node_data_returns_when_pipeline_is_missing(self): dataset = SimpleNamespace(runtime_mode="rag_pipeline", pipeline_id="pipeline-1") with patch("services.dataset_service.db") as mock_db: - mock_db.session.query.return_value.filter_by.return_value.first.return_value = None + mock_db.session.get.return_value = None DatasetService._update_pipeline_knowledge_base_node_data(dataset, "user-1") @@ -703,7 +709,7 @@ class TestDatasetServiceCreationAndUpdate: patch("services.dataset_service.RagPipelineService", return_value=rag_pipeline_service), patch("services.dataset_service.Workflow.new", return_value=new_workflow) as workflow_new, ): - mock_db.session.query.return_value.filter_by.return_value.first.return_value = pipeline + mock_db.session.get.return_value = pipeline DatasetService._update_pipeline_knowledge_base_node_data(dataset, "user-1") @@ -725,7 +731,7 @@ class TestDatasetServiceCreationAndUpdate: patch("services.dataset_service.db") as mock_db, patch("services.dataset_service.RagPipelineService", return_value=rag_pipeline_service), ): - mock_db.session.query.return_value.filter_by.return_value.first.return_value = pipeline + mock_db.session.get.return_value = pipeline with pytest.raises(RuntimeError, match="boom"): DatasetService._update_pipeline_knowledge_base_node_data(dataset, "user-1") @@ -1364,7 +1370,7 @@ class TestDatasetServicePermissionsAndLifecycle: ) with patch("services.dataset_service.db") as mock_db: - mock_db.session.query.return_value.filter_by.return_value.first.return_value = None + mock_db.session.scalar.return_value = None with pytest.raises(NoPermissionError, match="do not have permission"): DatasetService.check_dataset_permission(dataset, user) @@ -1382,7 +1388,7 @@ class TestDatasetServicePermissionsAndLifecycle: with patch("services.dataset_service.db") as mock_db: DatasetService.check_dataset_permission(dataset, user) - mock_db.session.query.assert_not_called() + mock_db.session.scalar.assert_not_called() def test_check_dataset_permission_allows_partial_team_member_with_binding(self): dataset = DatasetServiceUnitDataFactory.create_dataset_mock( @@ -1395,7 +1401,7 @@ class TestDatasetServicePermissionsAndLifecycle: ) with patch("services.dataset_service.db") as mock_db: - mock_db.session.query.return_value.filter_by.return_value.first.return_value = object() + mock_db.session.scalar.return_value = object() DatasetService.check_dataset_permission(dataset, user) @@ -1427,7 +1433,7 @@ class TestDatasetServicePermissionsAndLifecycle: ) with patch("services.dataset_service.db") as mock_db: - mock_db.session.query.return_value.filter_by.return_value.all.return_value = [] + mock_db.session.scalars.return_value.all.return_value = [] with pytest.raises(NoPermissionError, match="do not have permission"): DatasetService.check_dataset_operator_permission(user=user, dataset=dataset) @@ -1446,9 +1452,7 @@ class TestDatasetServicePermissionsAndLifecycle: def test_get_related_apps_returns_ordered_query_results(self): with patch("services.dataset_service.db") as mock_db: mock_db.desc.side_effect = lambda column: column - mock_db.session.query.return_value.where.return_value.order_by.return_value.all.return_value = [ - "relation-1" - ] + mock_db.session.scalars.return_value.all.return_value = ["relation-1"] result = DatasetService.get_related_apps("dataset-1") @@ -1610,7 +1614,7 @@ class TestDatasetCollectionBindingService: binding = SimpleNamespace(id="binding-1") with patch("services.dataset_service.db") as mock_db: - mock_db.session.query.return_value.where.return_value.order_by.return_value.first.return_value = binding + mock_db.session.scalar.return_value = binding result = DatasetCollectionBindingService.get_dataset_collection_binding("provider", "model") @@ -1622,10 +1626,11 @@ class TestDatasetCollectionBindingService: with ( patch("services.dataset_service.db") as mock_db, + patch("services.dataset_service.select"), patch("services.dataset_service.DatasetCollectionBinding", return_value=created_binding) as binding_cls, patch.object(Dataset, "gen_collection_name_by_id", return_value="generated-collection"), ): - mock_db.session.query.return_value.where.return_value.order_by.return_value.first.return_value = None + mock_db.session.scalar.return_value = None result = DatasetCollectionBindingService.get_dataset_collection_binding("provider", "model", "dataset") @@ -1641,7 +1646,7 @@ class TestDatasetCollectionBindingService: def test_get_dataset_collection_binding_by_id_and_type_raises_when_missing(self): with patch("services.dataset_service.db") as mock_db: - mock_db.session.query.return_value.where.return_value.order_by.return_value.first.return_value = None + mock_db.session.scalar.return_value = None with pytest.raises(ValueError, match="Dataset collection binding not found"): DatasetCollectionBindingService.get_dataset_collection_binding_by_id_and_type("binding-1") @@ -1650,7 +1655,7 @@ class TestDatasetCollectionBindingService: binding = SimpleNamespace(id="binding-1") with patch("services.dataset_service.db") as mock_db: - mock_db.session.query.return_value.where.return_value.order_by.return_value.first.return_value = binding + mock_db.session.scalar.return_value = binding result = DatasetCollectionBindingService.get_dataset_collection_binding_by_id_and_type("binding-1") @@ -1676,7 +1681,7 @@ class TestDatasetPermissionService: [{"user_id": "user-1"}, {"user_id": "user-2"}], ) - mock_db.session.query.return_value.where.return_value.delete.assert_called_once() + mock_db.session.execute.assert_called() mock_db.session.add_all.assert_called_once() mock_db.session.commit.assert_called_once() @@ -1747,12 +1752,12 @@ class TestDatasetPermissionService: with patch("services.dataset_service.db") as mock_db: DatasetPermissionService.clear_partial_member_list("dataset-1") - mock_db.session.query.return_value.where.return_value.delete.assert_called_once() + mock_db.session.execute.assert_called() mock_db.session.commit.assert_called_once() def test_clear_partial_member_list_rolls_back_on_exception(self): with patch("services.dataset_service.db") as mock_db: - mock_db.session.query.return_value.where.return_value.delete.side_effect = RuntimeError("boom") + mock_db.session.execute.side_effect = RuntimeError("boom") with pytest.raises(RuntimeError, match="boom"): DatasetPermissionService.clear_partial_member_list("dataset-1") diff --git a/api/tests/unit_tests/services/test_dataset_service_document.py b/api/tests/unit_tests/services/test_dataset_service_document.py index c8036487ab..e5a2541da7 100644 --- a/api/tests/unit_tests/services/test_dataset_service_document.py +++ b/api/tests/unit_tests/services/test_dataset_service_document.py @@ -90,13 +90,13 @@ class TestDocumentServiceQueryAndDownloadHelpers: result = DocumentService.get_document("dataset-1", None) assert result is None - mock_db.session.query.assert_not_called() + mock_db.session.scalar.assert_not_called() def test_get_document_queries_by_dataset_and_document_id(self): document = DatasetServiceUnitDataFactory.create_document_mock() with patch("services.dataset_service.db") as mock_db: - mock_db.session.query.return_value.where.return_value.first.return_value = document + mock_db.session.scalar.return_value = document result = DocumentService.get_document("dataset-1", "doc-1") @@ -435,7 +435,7 @@ class TestDocumentServiceQueryAndDownloadHelpers: upload_file = DatasetServiceUnitDataFactory.create_upload_file_mock() with patch("services.dataset_service.db") as mock_db: - mock_db.session.query.return_value.where.return_value.one_or_none.return_value = upload_file + mock_db.session.get.return_value = upload_file result = DocumentService.get_document_file_detail(upload_file.id) @@ -570,7 +570,7 @@ class TestDocumentServiceMutations: assert document.name == "New Name" assert document.doc_metadata[BuiltInField.document_name] == "New Name" mock_db.session.add.assert_called_once_with(document) - mock_db.session.query.return_value.where.return_value.update.assert_called_once() + mock_db.session.execute.assert_called() mock_db.session.commit.assert_called_once() def test_recover_document_raises_when_document_is_not_paused(self): @@ -624,9 +624,7 @@ class TestDocumentServiceMutations: document = DatasetServiceUnitDataFactory.create_document_mock(position=7) with patch("services.dataset_service.db") as mock_db: - mock_db.session.query.return_value.filter_by.return_value.order_by.return_value.first.return_value = ( - document - ) + mock_db.session.scalar.return_value = document result = DocumentService.get_documents_position("dataset-1") @@ -634,7 +632,7 @@ class TestDocumentServiceMutations: def test_get_documents_position_defaults_to_one_when_dataset_is_empty(self): with patch("services.dataset_service.db") as mock_db: - mock_db.session.query.return_value.filter_by.return_value.order_by.return_value.first.return_value = None + mock_db.session.scalar.return_value = None result = DocumentService.get_documents_position("dataset-1") @@ -869,11 +867,7 @@ class TestDocumentServiceUpdateDocumentWithDatasetId: patch("services.dataset_service.naive_utc_now", return_value="now"), patch("services.dataset_service.document_indexing_update_task") as update_task, ): - upload_query = MagicMock() - upload_query.where.return_value.first.return_value = SimpleNamespace(id="file-1", name="upload.txt") - segment_query = MagicMock() - segment_query.filter_by.return_value.update.return_value = 3 - mock_db.session.query.side_effect = [upload_query, segment_query] + mock_db.session.scalar.return_value = SimpleNamespace(id="file-1", name="upload.txt") result = DocumentService.update_document_with_dataset_id(dataset, document_data, account_context) @@ -892,7 +886,7 @@ class TestDocumentServiceUpdateDocumentWithDatasetId: assert document.created_from == "web" assert document.doc_form == IndexStructureType.QA_INDEX assert mock_db.session.commit.call_count == 3 - segment_query.filter_by.return_value.update.assert_called_once() + mock_db.session.execute.assert_called() update_task.delay.assert_called_once_with(document.dataset_id, document.id) def test_update_document_with_dataset_id_notion_import_requires_binding(self, account_context): @@ -920,9 +914,7 @@ class TestDocumentServiceUpdateDocumentWithDatasetId: patch.object(DatasetService, "check_dataset_model_setting"), patch("services.dataset_service.db") as mock_db, ): - binding_query = MagicMock() - binding_query.where.return_value.first.return_value = None - mock_db.session.query.return_value = binding_query + mock_db.session.scalar.return_value = None with pytest.raises(ValueError, match="Data source binding not found"): DocumentService.update_document_with_dataset_id(dataset, document_data, account_context) @@ -954,10 +946,6 @@ class TestDocumentServiceUpdateDocumentWithDatasetId: patch("services.dataset_service.naive_utc_now", return_value="now"), patch("services.dataset_service.document_indexing_update_task") as update_task, ): - segment_query = MagicMock() - segment_query.filter_by.return_value.update.return_value = 2 - mock_db.session.query.return_value = segment_query - result = DocumentService.update_document_with_dataset_id(dataset, document_data, account_context) assert result is document @@ -968,7 +956,7 @@ class TestDocumentServiceUpdateDocumentWithDatasetId: ) assert document.name == "" assert document.doc_form == IndexStructureType.PARENT_CHILD_INDEX - segment_query.filter_by.return_value.update.assert_called_once() + mock_db.session.execute.assert_called() update_task.delay.assert_called_once_with("dataset-1", "doc-1") @@ -1218,11 +1206,10 @@ class TestDocumentServiceSaveDocumentWithDatasetId: patch("services.dataset_service.secrets.randbelow", return_value=23), ): mock_redis.lock.return_value = _make_lock_context() - upload_query = MagicMock() - upload_query.where.return_value.all.return_value = [upload_file_a, upload_file_b] - existing_documents_query = MagicMock() - existing_documents_query.where.return_value.all.return_value = [duplicate_document] - mock_db.session.query.side_effect = [upload_query, existing_documents_query] + mock_db.session.scalars.return_value.all.side_effect = [ + [upload_file_a, upload_file_b], + [duplicate_document], + ] documents, batch = DocumentService.save_document_with_dataset_id( dataset, @@ -1302,9 +1289,7 @@ class TestDocumentServiceSaveDocumentWithDatasetId: patch("services.dataset_service.DocumentIndexingTaskProxy") as document_proxy_cls, ): mock_redis.lock.return_value = _make_lock_context() - notion_documents_query = MagicMock() - notion_documents_query.filter_by.return_value.all.return_value = [existing_keep, existing_remove] - mock_db.session.query.return_value = notion_documents_query + mock_db.session.scalars.return_value.all.return_value = [existing_keep, existing_remove] documents, _ = DocumentService.save_document_with_dataset_id( dataset, @@ -1474,12 +1459,11 @@ class TestDocumentServiceTenantAndUpdateEdges: def test_get_tenant_documents_count_returns_query_count(self, account_context): with patch("services.dataset_service.db") as mock_db: - mock_db.session.query.return_value.where.return_value.count.return_value = 12 + mock_db.session.scalar.return_value = 12 result = DocumentService.get_tenant_documents_count() assert result == 12 - mock_db.session.query.return_value.where.return_value.count.assert_called_once() def test_update_document_with_dataset_id_uses_automatic_process_rule_payload(self, account_context): dataset = SimpleNamespace(id="dataset-1", tenant_id="tenant-1") @@ -1514,11 +1498,7 @@ class TestDocumentServiceTenantAndUpdateEdges: ): process_rule_cls.AUTOMATIC_RULES = DatasetProcessRule.AUTOMATIC_RULES process_rule_cls.return_value = created_process_rule - upload_query = MagicMock() - upload_query.where.return_value.first.return_value = SimpleNamespace(id="file-1", name="upload.txt") - segment_query = MagicMock() - segment_query.filter_by.return_value.update.return_value = 1 - mock_db.session.query.side_effect = [upload_query, segment_query] + mock_db.session.scalar.return_value = SimpleNamespace(id="file-1", name="upload.txt") result = DocumentService.update_document_with_dataset_id(dataset, document_data, account_context) @@ -1567,7 +1547,7 @@ class TestDocumentServiceTenantAndUpdateEdges: patch.object(DatasetService, "check_dataset_model_setting"), patch("services.dataset_service.db") as mock_db, ): - mock_db.session.query.return_value.where.return_value.first.return_value = None + mock_db.session.scalar.return_value = None with pytest.raises(FileNotExistsError): DocumentService.update_document_with_dataset_id(dataset, document_data, account_context) @@ -1618,11 +1598,7 @@ class TestDocumentServiceTenantAndUpdateEdges: patch("services.dataset_service.naive_utc_now", return_value="now"), patch("services.dataset_service.document_indexing_update_task") as update_task, ): - binding_query = MagicMock() - binding_query.where.return_value.first.return_value = SimpleNamespace(id="binding-1") - segment_query = MagicMock() - segment_query.filter_by.return_value.update.return_value = 1 - mock_db.session.query.side_effect = [binding_query, segment_query] + mock_db.session.scalar.return_value = SimpleNamespace(id="binding-1") result = DocumentService.update_document_with_dataset_id(dataset, document_data, account_context) @@ -1914,11 +1890,7 @@ class TestDocumentServiceSaveDocumentAdditionalBranches: ): mock_redis.lock.return_value = _make_lock_context() process_rule_cls.return_value = created_process_rule - upload_query = MagicMock() - upload_query.where.return_value.all.return_value = [SimpleNamespace(id="file-1", name="file.txt")] - existing_documents_query = MagicMock() - existing_documents_query.where.return_value.all.return_value = [] - mock_db.session.query.side_effect = [upload_query, existing_documents_query] + mock_db.session.scalars.return_value.all.side_effect = [[SimpleNamespace(id="file-1", name="file.txt")], []] documents, batch = DocumentService.save_document_with_dataset_id(dataset, knowledge_config, account_context) @@ -1958,11 +1930,7 @@ class TestDocumentServiceSaveDocumentAdditionalBranches: mock_redis.lock.return_value = _make_lock_context() process_rule_cls.AUTOMATIC_RULES = DatasetProcessRule.AUTOMATIC_RULES process_rule_cls.return_value = created_process_rule - upload_query = MagicMock() - upload_query.where.return_value.all.return_value = [SimpleNamespace(id="file-1", name="file.txt")] - existing_documents_query = MagicMock() - existing_documents_query.where.return_value.all.return_value = [] - mock_db.session.query.side_effect = [upload_query, existing_documents_query] + mock_db.session.scalars.return_value.all.side_effect = [[SimpleNamespace(id="file-1", name="file.txt")], []] DocumentService.save_document_with_dataset_id(dataset, knowledge_config, account_context) @@ -1996,11 +1964,7 @@ class TestDocumentServiceSaveDocumentAdditionalBranches: mock_redis.lock.return_value = _make_lock_context() process_rule_cls.AUTOMATIC_RULES = DatasetProcessRule.AUTOMATIC_RULES process_rule_cls.return_value = created_process_rule - upload_query = MagicMock() - upload_query.where.return_value.all.return_value = [SimpleNamespace(id="file-1", name="file.txt")] - existing_documents_query = MagicMock() - existing_documents_query.where.return_value.all.return_value = [] - mock_db.session.query.side_effect = [upload_query, existing_documents_query] + mock_db.session.scalars.return_value.all.side_effect = [[SimpleNamespace(id="file-1", name="file.txt")], []] DocumentService.save_document_with_dataset_id(dataset, knowledge_config, account_context) @@ -2024,9 +1988,7 @@ class TestDocumentServiceSaveDocumentAdditionalBranches: patch("services.dataset_service.secrets.randbelow", return_value=23), ): mock_redis.lock.return_value = _make_lock_context() - upload_query = MagicMock() - upload_query.where.return_value.all.return_value = [SimpleNamespace(id="file-1", name="file.txt")] - mock_db.session.query.return_value = upload_query + mock_db.session.scalars.return_value.all.return_value = [SimpleNamespace(id="file-1", name="file.txt")] with pytest.raises(FileNotExistsError, match="One or more files not found"): DocumentService.save_document_with_dataset_id(dataset, knowledge_config, account_context) diff --git a/api/tests/unit_tests/services/test_dataset_service_segment.py b/api/tests/unit_tests/services/test_dataset_service_segment.py index 2f8ae14a8e..d6c104708c 100644 --- a/api/tests/unit_tests/services/test_dataset_service_segment.py +++ b/api/tests/unit_tests/services/test_dataset_service_segment.py @@ -49,7 +49,7 @@ class TestSegmentServiceChildChunks: patch("services.dataset_service.VectorService") as vector_service, ): mock_redis.lock.return_value = _make_lock_context() - mock_db.session.query.return_value.where.return_value.scalar.return_value = 2 + mock_db.session.scalar.return_value = 2 child_chunk = SegmentService.create_child_chunk("child content", segment, document, dataset) @@ -75,7 +75,7 @@ class TestSegmentServiceChildChunks: patch("services.dataset_service.VectorService") as vector_service, ): mock_redis.lock.return_value = _make_lock_context() - mock_db.session.query.return_value.where.return_value.scalar.return_value = None + mock_db.session.scalar.return_value = None vector_service.create_child_chunk_vector.side_effect = RuntimeError("vector failed") with pytest.raises(ChildChunkIndexingError, match="vector failed"): @@ -247,13 +247,13 @@ class TestSegmentServiceQueries: child_chunk = _make_child_chunk() with patch("services.dataset_service.db") as mock_db: - mock_db.session.query.return_value.where.return_value.first.return_value = child_chunk + mock_db.session.scalar.return_value = child_chunk result = SegmentService.get_child_chunk_by_id("child-a", "tenant-1") assert result is child_chunk with patch("services.dataset_service.db") as mock_db: - mock_db.session.query.return_value.where.return_value.first.return_value = SimpleNamespace() + mock_db.session.scalar.return_value = SimpleNamespace() result = SegmentService.get_child_chunk_by_id("child-a", "tenant-1") assert result is None @@ -295,13 +295,13 @@ class TestSegmentServiceQueries: ) with patch("services.dataset_service.db") as mock_db: - mock_db.session.query.return_value.where.return_value.first.return_value = segment + mock_db.session.scalar.return_value = segment result = SegmentService.get_segment_by_id("segment-1", "tenant-1") assert result is segment with patch("services.dataset_service.db") as mock_db: - mock_db.session.query.return_value.where.return_value.first.return_value = SimpleNamespace() + mock_db.session.scalar.return_value = SimpleNamespace() result = SegmentService.get_segment_by_id("segment-1", "tenant-1") assert result is None @@ -401,11 +401,8 @@ class TestSegmentServiceMutations: ): mock_redis.lock.return_value = _make_lock_context() - max_position_query = MagicMock() - max_position_query.where.return_value.scalar.return_value = 2 - refresh_query = MagicMock() - refresh_query.where.return_value.first.return_value = refreshed_segment - mock_db.session.query.side_effect = [max_position_query, refresh_query] + mock_db.session.scalar.return_value = 2 + mock_db.session.get.return_value = refreshed_segment def add_side_effect(obj): if obj.__class__.__name__ == "DocumentSegment" and getattr(obj, "id", None) is None: @@ -461,7 +458,7 @@ class TestSegmentServiceMutations: ): mock_redis.lock.return_value = _make_lock_context() model_manager_cls.for_tenant.return_value.get_model_instance.return_value = embedding_model - mock_db.session.query.return_value.where.return_value.scalar.return_value = 1 + mock_db.session.scalar.return_value = 1 vector_service.create_segments_vector.side_effect = RuntimeError("vector failed") result = SegmentService.multi_create_segment(segments, document, dataset) @@ -538,7 +535,7 @@ class TestSegmentServiceMutations: patch("services.dataset_service.VectorService") as vector_service, ): mock_redis.get.return_value = None - mock_db.session.query.return_value.where.return_value.first.return_value = refreshed_segment + mock_db.session.get.return_value = refreshed_segment result = SegmentService.update_segment(args, segment, document, dataset) @@ -574,13 +571,10 @@ class TestSegmentServiceMutations: mock_redis.get.return_value = None model_manager_cls.for_tenant.return_value.get_model_instance.return_value = embedding_model_instance - processing_rule_query = MagicMock() - processing_rule_query.where.return_value.first.return_value = processing_rule - summary_query = MagicMock() - summary_query.where.return_value.first.return_value = existing_summary - refreshed_query = MagicMock() - refreshed_query.where.return_value.first.return_value = refreshed_segment - mock_db.session.query.side_effect = [processing_rule_query, summary_query, refreshed_query] + # get calls: processing_rule, then refreshed_segment + mock_db.session.get.side_effect = [processing_rule, refreshed_segment] + # scalar call: existing_summary + mock_db.session.scalar.return_value = existing_summary result = SegmentService.update_segment(args, segment, document, dataset) @@ -621,11 +615,8 @@ class TestSegmentServiceMutations: mock_redis.get.return_value = None model_manager_cls.for_tenant.return_value.get_model_instance.return_value = embedding_model - summary_query = MagicMock() - summary_query.where.return_value.first.return_value = existing_summary - refreshed_query = MagicMock() - refreshed_query.where.return_value.first.return_value = refreshed_segment - mock_db.session.query.side_effect = [summary_query, refreshed_query] + mock_db.session.scalar.return_value = existing_summary + mock_db.session.get.return_value = refreshed_segment result = SegmentService.update_segment(args, segment, document, dataset) @@ -664,11 +655,8 @@ class TestSegmentServiceMutations: mock_redis.get.return_value = None model_manager_cls.for_tenant.return_value.get_model_instance.return_value = embedding_model - summary_query = MagicMock() - summary_query.where.return_value.first.return_value = existing_summary - refreshed_query = MagicMock() - refreshed_query.where.return_value.first.return_value = refreshed_segment - mock_db.session.query.side_effect = [summary_query, refreshed_query] + mock_db.session.scalar.return_value = existing_summary + mock_db.session.get.return_value = refreshed_segment result = SegmentService.update_segment(args, segment, document, dataset) @@ -688,7 +676,7 @@ class TestSegmentServiceMutations: patch("services.dataset_service.delete_segment_from_index_task") as delete_task, ): mock_redis.get.return_value = None - mock_db.session.query.return_value.where.return_value.all.return_value = [("child-1",), ("child-2",)] + mock_db.session.scalars.return_value.all.return_value = ["child-1", "child-2"] SegmentService.delete_segment(segment, document, dataset) @@ -727,15 +715,15 @@ class TestSegmentServiceMutations: patch("services.dataset_service.delete_segment_from_index_task") as delete_task, ): segments_query = MagicMock() - segments_query.with_entities.return_value.where.return_value.all.return_value = [ + # execute().all() for segments_info (multi-column) + execute_result = MagicMock() + execute_result.all.return_value = [ ("node-1", "segment-1", 2), ("node-2", "segment-2", 5), ] - child_query = MagicMock() - child_query.where.return_value.all.return_value = [("child-1",)] - delete_query = MagicMock() - delete_query.where.return_value.delete.return_value = 2 - mock_db.session.query.side_effect = [segments_query, child_query, delete_query] + mock_db.session.execute.return_value = execute_result + # scalars() for child_node_ids + mock_db.session.scalars.return_value.all.return_value = ["child-1"] SegmentService.delete_segments(["segment-1", "segment-2"], document, dataset) @@ -748,7 +736,6 @@ class TestSegmentServiceMutations: ["segment-1", "segment-2"], ["child-1"], ) - delete_query.where.return_value.delete.assert_called_once() mock_db.session.commit.assert_called_once() def test_update_segments_status_enables_only_segments_without_indexing_cache(self): @@ -868,7 +855,7 @@ class TestSegmentServiceAdditionalRegenerationBranches: patch("services.dataset_service.VectorService") as vector_service, ): mock_redis.get.return_value = None - mock_db.session.query.return_value.where.return_value.first.return_value = refreshed_segment + mock_db.session.get.return_value = refreshed_segment result = SegmentService.update_segment( SegmentUpdateArgs(content="question", answer="new answer"), @@ -902,11 +889,8 @@ class TestSegmentServiceAdditionalRegenerationBranches: ): mock_redis.get.return_value = None model_manager_cls.for_tenant.return_value.get_model_instance.return_value = embedding_model - summary_query = MagicMock() - summary_query.where.return_value.first.return_value = None - refreshed_query = MagicMock() - refreshed_query.where.return_value.first.return_value = refreshed_segment - mock_db.session.query.side_effect = [summary_query, refreshed_query] + mock_db.session.scalar.return_value = None + mock_db.session.get.return_value = refreshed_segment result = SegmentService.update_segment( SegmentUpdateArgs(content="new question", answer="new answer", keywords=["kw-1"]), @@ -951,13 +935,10 @@ class TestSegmentServiceAdditionalRegenerationBranches: model_manager_cls.for_tenant.return_value.get_default_model_instance.return_value = embedding_model_instance update_summary.side_effect = RuntimeError("summary failed") - processing_rule_query = MagicMock() - processing_rule_query.where.return_value.first.return_value = processing_rule - summary_query = MagicMock() - summary_query.where.return_value.first.return_value = existing_summary - refreshed_query = MagicMock() - refreshed_query.where.return_value.first.return_value = refreshed_segment - mock_db.session.query.side_effect = [processing_rule_query, summary_query, refreshed_query] + # get calls: processing_rule, then refreshed_segment + mock_db.session.get.side_effect = [processing_rule, refreshed_segment] + # scalar call: existing_summary + mock_db.session.scalar.return_value = existing_summary result = SegmentService.update_segment( SegmentUpdateArgs(content="new parent content", regenerate_child_chunks=True, summary="new summary"), @@ -1000,7 +981,7 @@ class TestSegmentServiceAdditionalRegenerationBranches: patch("services.dataset_service.VectorService") as vector_service, ): mock_redis.get.return_value = None - mock_db.session.query.return_value.where.return_value.first.return_value = refreshed_segment + mock_db.session.get.return_value = refreshed_segment result = SegmentService.update_segment( SegmentUpdateArgs(content="same content", regenerate_child_chunks=True), diff --git a/api/tests/unit_tests/services/test_datasource_provider_service.py b/api/tests/unit_tests/services/test_datasource_provider_service.py index da414816ff..bc4120e2af 100644 --- a/api/tests/unit_tests/services/test_datasource_provider_service.py +++ b/api/tests/unit_tests/services/test_datasource_provider_service.py @@ -57,6 +57,10 @@ class TestDatasourceProviderService: q.count.return_value = 0 q.delete.return_value = 1 + # Default values for select()-style calls (tests override per-case) + sess.scalar.return_value = None + sess.scalars.return_value.all.return_value = [] + mock_cls.return_value.__enter__.return_value = sess mock_cls.return_value.no_autoflush.__enter__.return_value = sess @@ -183,11 +187,11 @@ class TestDatasourceProviderService: # ----------------------------------------------------------------------- def test_should_return_true_when_tenant_oauth_params_enabled(self, service, mock_db_session): - mock_db_session.query().count.return_value = 1 + mock_db_session.scalar.return_value = 1 assert service.is_tenant_oauth_params_enabled("t1", make_id()) is True def test_should_return_false_when_tenant_oauth_params_disabled(self, service, mock_db_session): - mock_db_session.query().count.return_value = 0 + mock_db_session.scalar.return_value = 0 assert service.is_tenant_oauth_params_enabled("t1", make_id()) is False # ----------------------------------------------------------------------- @@ -401,7 +405,7 @@ class TestDatasourceProviderService: def test_should_return_masked_credentials_when_mask_is_true(self, service, mock_db_session): tenant_params = MagicMock() tenant_params.client_params = {"k": "v"} - mock_db_session.query().first.return_value = tenant_params + mock_db_session.scalar.return_value = tenant_params with patch.object(service, "get_oauth_encrypter", return_value=(self._enc, None)): result = service.get_tenant_oauth_client("t1", make_id(), mask=True) assert result == {"k": "mask"} @@ -409,13 +413,13 @@ class TestDatasourceProviderService: def test_should_return_decrypted_credentials_when_mask_is_false(self, service, mock_db_session): tenant_params = MagicMock() tenant_params.client_params = {"k": "v"} - mock_db_session.query().first.return_value = tenant_params + mock_db_session.scalar.return_value = tenant_params with patch.object(service, "get_oauth_encrypter", return_value=(self._enc, None)): result = service.get_tenant_oauth_client("t1", make_id(), mask=False) assert result == {"k": "dec"} def test_should_return_none_when_no_tenant_oauth_config_exists(self, service, mock_db_session): - mock_db_session.query().first.return_value = None + mock_db_session.scalar.return_value = None assert service.get_tenant_oauth_client("t1", make_id()) is None # ----------------------------------------------------------------------- @@ -616,7 +620,7 @@ class TestDatasourceProviderService: # ----------------------------------------------------------------------- def test_should_return_empty_list_when_no_credentials_stored(self, service, mock_db_session): - mock_db_session.query().all.return_value = [] + mock_db_session.scalars.return_value.all.return_value = [] assert service.list_datasource_credentials("t1", "prov", "org/plug") == [] def test_should_return_masked_credentials_list_when_credentials_exist(self, service, mock_db_session): @@ -624,7 +628,7 @@ class TestDatasourceProviderService: p.auth_type = "api_key" p.encrypted_credentials = {"sk": "v"} p.is_default = False - mock_db_session.query().all.return_value = [p] + mock_db_session.scalars.return_value.all.return_value = [p] with patch.object(service, "extract_secret_variables", return_value=["sk"]): result = service.list_datasource_credentials("t1", "prov", "org/plug") assert len(result) == 1 @@ -676,14 +680,14 @@ class TestDatasourceProviderService: # ----------------------------------------------------------------------- def test_should_return_empty_list_when_no_real_credentials_exist(self, service, mock_db_session): - mock_db_session.query().all.return_value = [] + mock_db_session.scalars.return_value.all.return_value = [] assert service.get_real_datasource_credentials("t1", "prov", "org/plug") == [] def test_should_return_decrypted_credential_list_when_credentials_exist(self, service, mock_db_session): p = MagicMock(spec=DatasourceProvider) p.auth_type = "api_key" p.encrypted_credentials = {"sk": "v"} - mock_db_session.query().all.return_value = [p] + mock_db_session.scalars.return_value.all.return_value = [p] with patch.object(service, "extract_secret_variables", return_value=["sk"]): result = service.get_real_datasource_credentials("t1", "prov", "org/plug") assert len(result) == 1 @@ -751,13 +755,13 @@ class TestDatasourceProviderService: def test_should_delete_provider_and_commit_when_found(self, service, mock_db_session): p = MagicMock(spec=DatasourceProvider) - mock_db_session.query().first.return_value = p + mock_db_session.scalar.return_value = p service.remove_datasource_credentials("t1", "id", "prov", "org/plug") mock_db_session.delete.assert_called_once_with(p) mock_db_session.commit.assert_called_once() def test_should_do_nothing_when_credential_not_found_on_remove(self, service, mock_db_session): """No error raised; no delete called when record doesn't exist (lines 994 branch).""" - mock_db_session.query().first.return_value = None + mock_db_session.scalar.return_value = None service.remove_datasource_credentials("t1", "id", "prov", "org/plug") mock_db_session.delete.assert_not_called() diff --git a/api/tests/unit_tests/services/test_external_dataset_service.py b/api/tests/unit_tests/services/test_external_dataset_service.py index 3709e1fa94..7c8dab5029 100644 --- a/api/tests/unit_tests/services/test_external_dataset_service.py +++ b/api/tests/unit_tests/services/test_external_dataset_service.py @@ -799,10 +799,7 @@ class TestExternalDatasetServiceGetAPI: api_id = "api-123" expected_api = factory.create_external_knowledge_api_mock(api_id=api_id) - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = expected_api + mock_db.session.scalar.return_value = expected_api # Act tenant_id = "tenant-123" @@ -810,16 +807,12 @@ class TestExternalDatasetServiceGetAPI: # Assert assert result.id == api_id - mock_query.filter_by.assert_called_once_with(id=api_id, tenant_id=tenant_id) @patch("services.external_knowledge_service.db") def test_get_external_knowledge_api_not_found(self, mock_db, factory): """Test error when API is not found.""" # Arrange - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(ValueError, match="api template not found"): @@ -848,10 +841,7 @@ class TestExternalDatasetServiceUpdateAPI: "settings": {"endpoint": "https://new.example.com", "api_key": "new-key"}, } - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = existing_api + mock_db.session.scalar.return_value = existing_api # Act result = ExternalDatasetService.update_external_knowledge_api(tenant_id, user_id, api_id, args) @@ -881,10 +871,7 @@ class TestExternalDatasetServiceUpdateAPI: "settings": {"endpoint": "https://api.example.com", "api_key": HIDDEN_VALUE}, } - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = existing_api + mock_db.session.scalar.return_value = existing_api # Act result = ExternalDatasetService.update_external_knowledge_api(tenant_id, "user-123", api_id, args) @@ -897,10 +884,7 @@ class TestExternalDatasetServiceUpdateAPI: def test_update_external_knowledge_api_not_found(self, mock_db, factory): """Test error when API is not found.""" # Arrange - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None + mock_db.session.scalar.return_value = None args = {"name": "Updated API"} @@ -912,10 +896,7 @@ class TestExternalDatasetServiceUpdateAPI: def test_update_external_knowledge_api_tenant_mismatch(self, mock_db, factory): """Test error when tenant ID doesn't match.""" # Arrange - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None + mock_db.session.scalar.return_value = None args = {"name": "Updated API"} @@ -934,10 +915,7 @@ class TestExternalDatasetServiceUpdateAPI: args = {"name": "New Name Only"} - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = existing_api + mock_db.session.scalar.return_value = existing_api # Act result = ExternalDatasetService.update_external_knowledge_api("tenant-123", "user-123", "api-123", args) @@ -958,10 +936,7 @@ class TestExternalDatasetServiceDeleteAPI: existing_api = factory.create_external_knowledge_api_mock(api_id=api_id, tenant_id=tenant_id) - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = existing_api + mock_db.session.scalar.return_value = existing_api # Act ExternalDatasetService.delete_external_knowledge_api(tenant_id, api_id) @@ -974,10 +949,7 @@ class TestExternalDatasetServiceDeleteAPI: def test_delete_external_knowledge_api_not_found(self, mock_db, factory): """Test error when API is not found.""" # Arrange - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(ValueError, match="api template not found"): @@ -987,10 +959,7 @@ class TestExternalDatasetServiceDeleteAPI: def test_delete_external_knowledge_api_tenant_mismatch(self, mock_db, factory): """Test error when tenant ID doesn't match.""" # Arrange - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(ValueError, match="api template not found"): @@ -1006,10 +975,7 @@ class TestExternalDatasetServiceAPIUseCheck: # Arrange api_id = "api-123" - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.count.return_value = 1 + mock_db.session.scalar.return_value = 1 # Act in_use, count = ExternalDatasetService.external_knowledge_api_use_check(api_id) @@ -1024,10 +990,7 @@ class TestExternalDatasetServiceAPIUseCheck: # Arrange api_id = "api-123" - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.count.return_value = 10 + mock_db.session.scalar.return_value = 10 # Act in_use, count = ExternalDatasetService.external_knowledge_api_use_check(api_id) @@ -1042,10 +1005,7 @@ class TestExternalDatasetServiceAPIUseCheck: # Arrange api_id = "api-123" - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.count.return_value = 0 + mock_db.session.scalar.return_value = 0 # Act in_use, count = ExternalDatasetService.external_knowledge_api_use_check(api_id) @@ -1067,10 +1027,7 @@ class TestExternalDatasetServiceGetBinding: expected_binding = factory.create_external_knowledge_binding_mock(tenant_id=tenant_id, dataset_id=dataset_id) - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = expected_binding + mock_db.session.scalar.return_value = expected_binding # Act result = ExternalDatasetService.get_external_knowledge_binding_with_dataset_id(tenant_id, dataset_id) @@ -1083,10 +1040,7 @@ class TestExternalDatasetServiceGetBinding: def test_get_external_knowledge_binding_not_found(self, mock_db, factory): """Test error when binding is not found.""" # Arrange - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(ValueError, match="external knowledge binding not found"): @@ -1113,10 +1067,7 @@ class TestExternalDatasetServiceDocumentValidate: api = factory.create_external_knowledge_api_mock(api_id=api_id, settings=[settings]) - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = api + mock_db.session.scalar.return_value = api process_parameter = {"param1": "value1", "param2": "value2"} @@ -1134,10 +1085,7 @@ class TestExternalDatasetServiceDocumentValidate: api = factory.create_external_knowledge_api_mock(api_id=api_id, settings=[settings]) - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = api + mock_db.session.scalar.return_value = api process_parameter = {} @@ -1149,10 +1097,7 @@ class TestExternalDatasetServiceDocumentValidate: def test_document_create_args_validate_api_not_found(self, mock_db, factory): """Test validation fails when API is not found.""" # Arrange - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(ValueError, match="api template not found"): @@ -1165,10 +1110,7 @@ class TestExternalDatasetServiceDocumentValidate: settings = {} api = factory.create_external_knowledge_api_mock(settings=[settings]) - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = api + mock_db.session.scalar.return_value = api # Act & Assert - should not raise ExternalDatasetService.document_create_args_validate("tenant-123", "api-123", {}) @@ -1186,10 +1128,7 @@ class TestExternalDatasetServiceDocumentValidate: api = factory.create_external_knowledge_api_mock(settings=[settings]) - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = api + mock_db.session.scalar.return_value = api process_parameter = {"required_param": "value"} @@ -1498,24 +1437,7 @@ class TestExternalDatasetServiceCreateDataset: api = factory.create_external_knowledge_api_mock(api_id="api-123") - # Mock database queries - mock_dataset_query = MagicMock() - mock_api_query = MagicMock() - - def query_side_effect(model): - if model == Dataset: - return mock_dataset_query - elif model == ExternalKnowledgeApis: - return mock_api_query - return MagicMock() - - mock_db.session.query.side_effect = query_side_effect - - mock_dataset_query.filter_by.return_value = mock_dataset_query - mock_dataset_query.first.return_value = None - - mock_api_query.filter_by.return_value = mock_api_query - mock_api_query.first.return_value = api + mock_db.session.scalar.side_effect = [None, api] # Act result = ExternalDatasetService.create_external_dataset(tenant_id, user_id, args) @@ -1534,10 +1456,7 @@ class TestExternalDatasetServiceCreateDataset: # Arrange existing_dataset = factory.create_dataset_mock(name="Duplicate Dataset") - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = existing_dataset + mock_db.session.scalar.return_value = existing_dataset args = {"name": "Duplicate Dataset"} @@ -1549,23 +1468,7 @@ class TestExternalDatasetServiceCreateDataset: def test_create_external_dataset_api_not_found_error(self, mock_db, factory): """Test error when external knowledge API is not found.""" # Arrange - mock_dataset_query = MagicMock() - mock_api_query = MagicMock() - - def query_side_effect(model): - if model == Dataset: - return mock_dataset_query - elif model == ExternalKnowledgeApis: - return mock_api_query - return MagicMock() - - mock_db.session.query.side_effect = query_side_effect - - mock_dataset_query.filter_by.return_value = mock_dataset_query - mock_dataset_query.first.return_value = None - - mock_api_query.filter_by.return_value = mock_api_query - mock_api_query.first.return_value = None + mock_db.session.scalar.side_effect = [None, None] args = {"name": "Test Dataset", "external_knowledge_api_id": "nonexistent-api"} @@ -1579,23 +1482,7 @@ class TestExternalDatasetServiceCreateDataset: # Arrange api = factory.create_external_knowledge_api_mock() - mock_dataset_query = MagicMock() - mock_api_query = MagicMock() - - def query_side_effect(model): - if model == Dataset: - return mock_dataset_query - elif model == ExternalKnowledgeApis: - return mock_api_query - return MagicMock() - - mock_db.session.query.side_effect = query_side_effect - - mock_dataset_query.filter_by.return_value = mock_dataset_query - mock_dataset_query.first.return_value = None - - mock_api_query.filter_by.return_value = mock_api_query - mock_api_query.first.return_value = api + mock_db.session.scalar.side_effect = [None, api] args = {"name": "Test Dataset", "external_knowledge_api_id": "api-123"} @@ -1609,23 +1496,7 @@ class TestExternalDatasetServiceCreateDataset: # Arrange api = factory.create_external_knowledge_api_mock() - mock_dataset_query = MagicMock() - mock_api_query = MagicMock() - - def query_side_effect(model): - if model == Dataset: - return mock_dataset_query - elif model == ExternalKnowledgeApis: - return mock_api_query - return MagicMock() - - mock_db.session.query.side_effect = query_side_effect - - mock_dataset_query.filter_by.return_value = mock_dataset_query - mock_dataset_query.first.return_value = None - - mock_api_query.filter_by.return_value = mock_api_query - mock_api_query.first.return_value = api + mock_db.session.scalar.side_effect = [None, api] args = {"name": "Test Dataset", "external_knowledge_id": "knowledge-123"} @@ -1651,23 +1522,7 @@ class TestExternalDatasetServiceFetchRetrieval: ) api = factory.create_external_knowledge_api_mock(api_id="api-123") - mock_binding_query = MagicMock() - mock_api_query = MagicMock() - - def query_side_effect(model): - if model == ExternalKnowledgeBindings: - return mock_binding_query - elif model == ExternalKnowledgeApis: - return mock_api_query - return MagicMock() - - mock_db.session.query.side_effect = query_side_effect - - mock_binding_query.filter_by.return_value = mock_binding_query - mock_binding_query.first.return_value = binding - - mock_api_query.filter_by.return_value = mock_api_query - mock_api_query.first.return_value = api + mock_db.session.scalar.side_effect = [binding, api] mock_response = MagicMock() mock_response.status_code = 200 @@ -1695,10 +1550,7 @@ class TestExternalDatasetServiceFetchRetrieval: def test_fetch_external_knowledge_retrieval_binding_not_found_error(self, mock_db, factory): """Test error when external knowledge binding is not found.""" # Arrange - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(ValueError, match="external knowledge binding not found"): @@ -1712,23 +1564,7 @@ class TestExternalDatasetServiceFetchRetrieval: binding = factory.create_external_knowledge_binding_mock() api = factory.create_external_knowledge_api_mock() - mock_binding_query = MagicMock() - mock_api_query = MagicMock() - - def query_side_effect(model): - if model == ExternalKnowledgeBindings: - return mock_binding_query - elif model == ExternalKnowledgeApis: - return mock_api_query - return MagicMock() - - mock_db.session.query.side_effect = query_side_effect - - mock_binding_query.filter_by.return_value = mock_binding_query - mock_binding_query.first.return_value = binding - - mock_api_query.filter_by.return_value = mock_api_query - mock_api_query.first.return_value = api + mock_db.session.scalar.side_effect = [binding, api] mock_response = MagicMock() mock_response.status_code = 200 @@ -1751,23 +1587,7 @@ class TestExternalDatasetServiceFetchRetrieval: binding = factory.create_external_knowledge_binding_mock() api = factory.create_external_knowledge_api_mock() - mock_binding_query = MagicMock() - mock_api_query = MagicMock() - - def query_side_effect(model): - if model == ExternalKnowledgeBindings: - return mock_binding_query - elif model == ExternalKnowledgeApis: - return mock_api_query - return MagicMock() - - mock_db.session.query.side_effect = query_side_effect - - mock_binding_query.filter_by.return_value = mock_binding_query - mock_binding_query.first.return_value = binding - - mock_api_query.filter_by.return_value = mock_api_query - mock_api_query.first.return_value = api + mock_db.session.scalar.side_effect = [binding, api] mock_response = MagicMock() mock_response.status_code = 200 @@ -1799,23 +1619,7 @@ class TestExternalDatasetServiceFetchRetrieval: binding = factory.create_external_knowledge_binding_mock() api = factory.create_external_knowledge_api_mock() - mock_binding_query = MagicMock() - mock_api_query = MagicMock() - - def query_side_effect(model): - if model == ExternalKnowledgeBindings: - return mock_binding_query - elif model == ExternalKnowledgeApis: - return mock_api_query - return MagicMock() - - mock_db.session.query.side_effect = query_side_effect - - mock_binding_query.filter_by.return_value = mock_binding_query - mock_binding_query.first.return_value = binding - - mock_api_query.filter_by.return_value = mock_api_query - mock_api_query.first.return_value = api + mock_db.session.scalar.side_effect = [binding, api] mock_response = MagicMock() mock_response.status_code = 500 @@ -1856,23 +1660,7 @@ class TestExternalDatasetServiceFetchRetrieval: ) api = factory.create_external_knowledge_api_mock(api_id="api-123") - mock_binding_query = MagicMock() - mock_api_query = MagicMock() - - def query_side_effect(model): - if model == ExternalKnowledgeBindings: - return mock_binding_query - elif model == ExternalKnowledgeApis: - return mock_api_query - return MagicMock() - - mock_db.session.query.side_effect = query_side_effect - - mock_binding_query.filter_by.return_value = mock_binding_query - mock_binding_query.first.return_value = binding - - mock_api_query.filter_by.return_value = mock_api_query - mock_api_query.first.return_value = api + mock_db.session.scalar.side_effect = [binding, api] mock_response = MagicMock() mock_response.status_code = status_code @@ -1891,23 +1679,7 @@ class TestExternalDatasetServiceFetchRetrieval: binding = factory.create_external_knowledge_binding_mock() api = factory.create_external_knowledge_api_mock() - mock_binding_query = MagicMock() - mock_api_query = MagicMock() - - def query_side_effect(model): - if model == ExternalKnowledgeBindings: - return mock_binding_query - elif model == ExternalKnowledgeApis: - return mock_api_query - return MagicMock() - - mock_db.session.query.side_effect = query_side_effect - - mock_binding_query.filter_by.return_value = mock_binding_query - mock_binding_query.first.return_value = binding - - mock_api_query.filter_by.return_value = mock_api_query - mock_api_query.first.return_value = api + mock_db.session.scalar.side_effect = [binding, api] mock_response = MagicMock() mock_response.status_code = 503 diff --git a/api/tests/unit_tests/services/test_schedule_service.py b/api/tests/unit_tests/services/test_schedule_service.py index 2a78876da6..334062242b 100644 --- a/api/tests/unit_tests/services/test_schedule_service.py +++ b/api/tests/unit_tests/services/test_schedule_service.py @@ -690,8 +690,8 @@ class TestSyncScheduleFromWorkflow(unittest.TestCase): mock_db.engine = MagicMock() mock_session.__enter__ = MagicMock(return_value=mock_session) mock_session.__exit__ = MagicMock(return_value=None) - Session = MagicMock(return_value=mock_session) - with patch("events.event_handlers.sync_workflow_schedule_when_app_published.Session", Session): + sessionmaker = MagicMock(return_value=MagicMock(begin=MagicMock(return_value=mock_session))) + with patch("events.event_handlers.sync_workflow_schedule_when_app_published.sessionmaker", sessionmaker): mock_session.scalar.return_value = None # No existing plan # Mock extract_schedule_config to return a ScheduleConfig object @@ -709,7 +709,7 @@ class TestSyncScheduleFromWorkflow(unittest.TestCase): assert result == mock_new_plan mock_service.create_schedule.assert_called_once() - mock_session.commit.assert_called_once() + mock_session.commit.assert_not_called() @patch("events.event_handlers.sync_workflow_schedule_when_app_published.db") @patch("events.event_handlers.sync_workflow_schedule_when_app_published.ScheduleService") @@ -720,9 +720,9 @@ class TestSyncScheduleFromWorkflow(unittest.TestCase): mock_db.engine = MagicMock() mock_session.__enter__ = MagicMock(return_value=mock_session) mock_session.__exit__ = MagicMock(return_value=None) - Session = MagicMock(return_value=mock_session) + sessionmaker = MagicMock(return_value=MagicMock(begin=MagicMock(return_value=mock_session))) - with patch("events.event_handlers.sync_workflow_schedule_when_app_published.Session", Session): + with patch("events.event_handlers.sync_workflow_schedule_when_app_published.sessionmaker", sessionmaker): mock_existing_plan = Mock(spec=WorkflowSchedulePlan) mock_existing_plan.id = "existing-plan-id" mock_session.scalar.return_value = mock_existing_plan @@ -751,7 +751,7 @@ class TestSyncScheduleFromWorkflow(unittest.TestCase): assert updates_obj.node_id == "start" assert updates_obj.cron_expression == "0 12 * * *" assert updates_obj.timezone == "America/New_York" - mock_session.commit.assert_called_once() + mock_session.commit.assert_not_called() @patch("events.event_handlers.sync_workflow_schedule_when_app_published.db") @patch("events.event_handlers.sync_workflow_schedule_when_app_published.ScheduleService") @@ -762,9 +762,9 @@ class TestSyncScheduleFromWorkflow(unittest.TestCase): mock_db.engine = MagicMock() mock_session.__enter__ = MagicMock(return_value=mock_session) mock_session.__exit__ = MagicMock(return_value=None) - Session = MagicMock(return_value=mock_session) + sessionmaker = MagicMock(return_value=MagicMock(begin=MagicMock(return_value=mock_session))) - with patch("events.event_handlers.sync_workflow_schedule_when_app_published.Session", Session): + with patch("events.event_handlers.sync_workflow_schedule_when_app_published.sessionmaker", sessionmaker): mock_existing_plan = Mock(spec=WorkflowSchedulePlan) mock_existing_plan.id = "existing-plan-id" mock_session.scalar.return_value = mock_existing_plan @@ -777,7 +777,7 @@ class TestSyncScheduleFromWorkflow(unittest.TestCase): assert result is None # Now using ScheduleService.delete_schedule instead of session.delete mock_service.delete_schedule.assert_called_once_with(session=mock_session, schedule_id="existing-plan-id") - mock_session.commit.assert_called_once() + mock_session.commit.assert_not_called() @pytest.fixture diff --git a/api/tests/unit_tests/services/test_webhook_service.py b/api/tests/unit_tests/services/test_webhook_service.py index 78049182ad..c86ed2debd 100644 --- a/api/tests/unit_tests/services/test_webhook_service.py +++ b/api/tests/unit_tests/services/test_webhook_service.py @@ -1100,12 +1100,11 @@ def test_trigger_workflow_execution_should_mark_tenant_rate_limited_when_quota_e "get_or_create_end_user_by_type", MagicMock(return_value=SimpleNamespace(id="end-user-1")), ) - quota_type = SimpleNamespace( - TRIGGER=SimpleNamespace( - consume=MagicMock(side_effect=QuotaExceededError(feature="trigger", tenant_id="tenant-1", required=1)) - ) + monkeypatch.setattr( + service_module.QuotaService, + "reserve", + MagicMock(side_effect=QuotaExceededError(feature="trigger", tenant_id="tenant-1", required=1)), ) - monkeypatch.setattr(service_module, "QuotaType", quota_type) mark_rate_limited_mock = MagicMock() monkeypatch.setattr(service_module.AppTriggerService, "mark_tenant_triggers_rate_limited", mark_rate_limited_mock) diff --git a/api/tests/unit_tests/tasks/test_delete_account_task.py b/api/tests/unit_tests/tasks/test_delete_account_task.py index 8a12a4a169..f949c13158 100644 --- a/api/tests/unit_tests/tasks/test_delete_account_task.py +++ b/api/tests/unit_tests/tasks/test_delete_account_task.py @@ -26,9 +26,6 @@ def mock_db_session(): cm.__exit__.return_value = None mock_sf.create_session.return_value = cm - query = MagicMock() - session.query.return_value = query - query.where.return_value = query yield session @@ -49,12 +46,12 @@ def mock_deps(): def _set_account_found(mock_db_session, email: str = "user@example.com"): account = SimpleNamespace(email=email) - mock_db_session.query.return_value.where.return_value.first.return_value = account + mock_db_session.scalar.return_value = account return account def _set_account_missing(mock_db_session): - mock_db_session.query.return_value.where.return_value.first.return_value = None + mock_db_session.scalar.return_value = None class TestDeleteAccountTask: diff --git a/api/uv.lock b/api/uv.lock index 9381fabb40..51424fc502 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -53,23 +53,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/45/4a/064321452809dae953c1ed6e017504e72551a26b6f5708a5a80e4bf556ff/aiohttp-3.13.4.tar.gz", hash = "sha256:d97a6d09c66087890c2ab5d49069e1e570583f7ac0314ecf98294c1b6aaebd38", size = 7859748, upload-time = "2026-03-28T17:19:40.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/7e/cb94129302d78c46662b47f9897d642fd0b33bdfef4b73b20c6ced35aa4c/aiohttp-3.13.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8ea0c64d1bcbf201b285c2246c51a0c035ba3bbd306640007bc5844a3b4658c1", size = 760027, upload-time = "2026-03-28T17:15:33.022Z" }, - { url = "https://files.pythonhosted.org/packages/5e/cd/2db3c9397c3bd24216b203dd739945b04f8b87bb036c640da7ddb63c75ef/aiohttp-3.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6f742e1fa45c0ed522b00ede565e18f97e4cf8d1883a712ac42d0339dfb0cce7", size = 508325, upload-time = "2026-03-28T17:15:34.714Z" }, - { url = "https://files.pythonhosted.org/packages/36/a3/d28b2722ec13107f2e37a86b8a169897308bab6a3b9e071ecead9d67bd9b/aiohttp-3.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dcfb50ee25b3b7a1222a9123be1f9f89e56e67636b561441f0b304e25aaef8f", size = 502402, upload-time = "2026-03-28T17:15:36.409Z" }, - { url = "https://files.pythonhosted.org/packages/fa/d6/acd47b5f17c4430e555590990a4746efbcb2079909bb865516892bf85f37/aiohttp-3.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3262386c4ff370849863ea93b9ea60fd59c6cf56bf8f93beac625cf4d677c04d", size = 1771224, upload-time = "2026-03-28T17:15:38.223Z" }, - { url = "https://files.pythonhosted.org/packages/98/af/af6e20113ba6a48fd1cd9e5832c4851e7613ef50c7619acdaee6ec5f1aff/aiohttp-3.13.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:473bb5aa4218dd254e9ae4834f20e31f5a0083064ac0136a01a62ddbae2eaa42", size = 1731530, upload-time = "2026-03-28T17:15:39.988Z" }, - { url = "https://files.pythonhosted.org/packages/81/16/78a2f5d9c124ad05d5ce59a9af94214b6466c3491a25fb70760e98e9f762/aiohttp-3.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56423766399b4c77b965f6aaab6c9546617b8994a956821cc507d00b91d978c", size = 1827925, upload-time = "2026-03-28T17:15:41.944Z" }, - { url = "https://files.pythonhosted.org/packages/2a/1f/79acf0974ced805e0e70027389fccbb7d728e6f30fcac725fb1071e63075/aiohttp-3.13.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8af249343fafd5ad90366a16d230fc265cf1149f26075dc9fe93cfd7c7173942", size = 1923579, upload-time = "2026-03-28T17:15:44.071Z" }, - { url = "https://files.pythonhosted.org/packages/af/53/29f9e2054ea6900413f3b4c3eb9d8331f60678ec855f13ba8714c47fd48d/aiohttp-3.13.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bc0a5cf4f10ef5a2c94fdde488734b582a3a7a000b131263e27c9295bd682d9", size = 1767655, upload-time = "2026-03-28T17:15:45.911Z" }, - { url = "https://files.pythonhosted.org/packages/f3/57/462fe1d3da08109ba4aa8590e7aed57c059af2a7e80ec21f4bac5cfe1094/aiohttp-3.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5c7ff1028e3c9fc5123a865ce17df1cb6424d180c503b8517afbe89aa566e6be", size = 1630439, upload-time = "2026-03-28T17:15:48.11Z" }, - { url = "https://files.pythonhosted.org/packages/d7/4b/4813344aacdb8127263e3eec343d24e973421143826364fa9fc847f6283f/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ba5cf98b5dcb9bddd857da6713a503fa6d341043258ca823f0f5ab7ab4a94ee8", size = 1745557, upload-time = "2026-03-28T17:15:50.13Z" }, - { url = "https://files.pythonhosted.org/packages/d4/01/1ef1adae1454341ec50a789f03cfafe4c4ac9c003f6a64515ecd32fe4210/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d85965d3ba21ee4999e83e992fecb86c4614d6920e40705501c0a1f80a583c12", size = 1741796, upload-time = "2026-03-28T17:15:52.351Z" }, - { url = "https://files.pythonhosted.org/packages/22/04/8cdd99af988d2aa6922714d957d21383c559835cbd43fbf5a47ddf2e0f05/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:49f0b18a9b05d79f6f37ddd567695943fcefb834ef480f17a4211987302b2dc7", size = 1805312, upload-time = "2026-03-28T17:15:54.407Z" }, - { url = "https://files.pythonhosted.org/packages/fb/7f/b48d5577338d4b25bbdbae35c75dbfd0493cb8886dc586fbfb2e90862239/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7f78cb080c86fbf765920e5f1ef35af3f24ec4314d6675d0a21eaf41f6f2679c", size = 1621751, upload-time = "2026-03-28T17:15:56.564Z" }, - { url = "https://files.pythonhosted.org/packages/bc/89/4eecad8c1858e6d0893c05929e22343e0ebe3aec29a8a399c65c3cc38311/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:67a3ec705534a614b68bbf1c70efa777a21c3da3895d1c44510a41f5a7ae0453", size = 1826073, upload-time = "2026-03-28T17:15:58.489Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5c/9dc8293ed31b46c39c9c513ac7ca152b3c3d38e0ea111a530ad12001b827/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6630ec917e85c5356b2295744c8a97d40f007f96a1c76bf1928dc2e27465393", size = 1760083, upload-time = "2026-03-28T17:16:00.677Z" }, - { url = "https://files.pythonhosted.org/packages/1e/19/8bbf6a4994205d96831f97b7d21a0feed120136e6267b5b22d229c6dc4dc/aiohttp-3.13.4-cp311-cp311-win32.whl", hash = "sha256:54049021bc626f53a5394c29e8c444f726ee5a14b6e89e0ad118315b1f90f5e3", size = 439690, upload-time = "2026-03-28T17:16:02.902Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f5/ac409ecd1007528d15c3e8c3a57d34f334c70d76cfb7128a28cffdebd4c1/aiohttp-3.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:c033f2bc964156030772d31cbf7e5defea181238ce1f87b9455b786de7d30145", size = 463824, upload-time = "2026-03-28T17:16:05.058Z" }, { url = "https://files.pythonhosted.org/packages/1e/bd/ede278648914cabbabfdf95e436679b5d4156e417896a9b9f4587169e376/aiohttp-3.13.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ee62d4471ce86b108b19c3364db4b91180d13fe3510144872d6bad5401957360", size = 752158, upload-time = "2026-03-28T17:16:06.901Z" }, { url = "https://files.pythonhosted.org/packages/90/de/581c053253c07b480b03785196ca5335e3c606a37dc73e95f6527f1591fe/aiohttp-3.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c0fd8f41b54b58636402eb493afd512c23580456f022c1ba2db0f810c959ed0d", size = 501037, upload-time = "2026-03-28T17:16:08.82Z" }, { url = "https://files.pythonhosted.org/packages/fa/f9/a5ede193c08f13cc42c0a5b50d1e246ecee9115e4cf6e900d8dbd8fd6acb/aiohttp-3.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4baa48ce49efd82d6b1a0be12d6a36b35e5594d1dd42f8bfba96ea9f8678b88c", size = 501556, upload-time = "2026-03-28T17:16:10.63Z" }, @@ -160,16 +143,16 @@ sdist = { url = "https://files.pythonhosted.org/packages/ab/98/d7111245f17935bf7 [[package]] name = "alibabacloud-gpdb20160503" -version = "5.1.0" +version = "5.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "alibabacloud-credentials" }, { name = "alibabacloud-tea-openapi" }, { name = "darabonba-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/36/69333c7fb7fb5267f338371b14fdd8dbdd503717c97bbc7a6419d155ab4c/alibabacloud_gpdb20160503-5.1.0.tar.gz", hash = "sha256:086ec6d5e39b64f54d0e44bb3fd4fde1a4822a53eb9f6ff7464dff7d19b07b63", size = 295641, upload-time = "2026-03-19T10:09:02.444Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/ba/606601479707f90138be38493b7b4d8457da10bbc58e84cd000108468a44/alibabacloud_gpdb20160503-5.2.0.tar.gz", hash = "sha256:d8f41bfcdc189f9d0283a87df2c3fa26a27617bc2d604652c7763bf9dd3ba22d", size = 299202, upload-time = "2026-04-02T19:27:25.639Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/7f/a91a2f9ad97c92fa9a6981587ea0ff789240cea05b17b17b7c244e5bac64/alibabacloud_gpdb20160503-5.1.0-py3-none-any.whl", hash = "sha256:580e4579285a54c7f04570782e0f60423a1997568684187fe88e4110acfb640e", size = 848784, upload-time = "2026-03-19T10:09:00.72Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a3/eee56773d22b8ee4039f2a4754bcf957631302d2e59e5b110cdd768e25ac/alibabacloud_gpdb20160503-5.2.0-py3-none-any.whl", hash = "sha256:b2bad9d2f7e0247985120c25f6cd42e75447fb9157dff817f64eae1734abcbd7", size = 857108, upload-time = "2026-04-02T19:27:24.446Z" }, ] [[package]] @@ -442,19 +425,19 @@ wheels = [ [[package]] name = "basedpyright" -version = "1.38.4" +version = "1.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodejs-wheel-binaries" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/b4/26cb812eaf8ab56909c792c005fe1690706aef6f21d61107639e46e9c54c/basedpyright-1.38.4.tar.gz", hash = "sha256:8e7d4f37ffb6106621e06b9355025009cdf5b48f71c592432dd2dd304bf55e70", size = 25354730, upload-time = "2026-03-25T13:50:44.353Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/f4/4a77cc1ffb3dab7391642cde30163961d8ee973e9e6b6740c7d15aa3d3ba/basedpyright-1.39.0.tar.gz", hash = "sha256:6666f51c378c7ac45877c4c1c7041ee0b5b83d755ebc82f898f47b6fafe0cc4f", size = 25357403, upload-time = "2026-04-01T12:27:41.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/0b/3f95fd47def42479e61077523d3752086d5c12009192a7f1c9fd5507e687/basedpyright-1.38.4-py3-none-any.whl", hash = "sha256:90aa067cf3e8a3c17ad5836a72b9e1f046bc72a4ad57d928473d9368c9cd07a2", size = 12352258, upload-time = "2026-03-25T13:50:41.059Z" }, + { url = "https://files.pythonhosted.org/packages/97/47/08145d1bcc3083ed20059bdecbde404bd767f91b91e2764ec01cffec9f4b/basedpyright-1.39.0-py3-none-any.whl", hash = "sha256:91b8ad50bc85ee4a985b928f9368c35c99eee5a56c44e99b2442fa12ecc3d670", size = 12353868, upload-time = "2026-04-01T12:27:38.495Z" }, ] [[package]] name = "bce-python-sdk" -version = "0.9.67" +version = "0.9.68" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "crc32c" }, @@ -462,9 +445,9 @@ dependencies = [ { name = "pycryptodome" }, { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/b9/5140cc02832fe3a7394c52949796d43f8c1f635aa016100f857f504e0348/bce_python_sdk-0.9.67.tar.gz", hash = "sha256:2c673d757c5c8952f1be6611da4ab77a63ecabaa3ff22b11531f46845ac99e58", size = 295251, upload-time = "2026-03-24T14:10:07.086Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/7c/8b4d9128e571f898f9f177dc9f41e31692d8ddb08a963b0c576f219d1304/bce_python_sdk-0.9.68.tar.gz", hash = "sha256:adf182868ed25e53cc3c1573dad9a2b1e9b72ed1ffd0d3ef326f5fa93da7cfa6", size = 296349, upload-time = "2026-03-30T02:57:32.948Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/a9/a58a63e2756e5d01901595af58c673f68de7621f28d71007479e00f45a6c/bce_python_sdk-0.9.67-py3-none-any.whl", hash = "sha256:3054879d098a92ceeb4b9ac1e64d2c658120a5a10e8e630f22410564b2170bf0", size = 410854, upload-time = "2026-03-24T14:09:54.29Z" }, + { url = "https://files.pythonhosted.org/packages/fa/4e/eaaba9264667d675c3de76485dc511f0f233c31bada8752411f7fc5170be/bce_python_sdk-0.9.68-py3-none-any.whl", hash = "sha256:fcb484db4a54aa2c4675834c10bc6c37d42929fd138faaf6c01f933d8fa927ed", size = 411932, upload-time = "2026-03-30T02:57:27.847Z" }, ] [[package]] @@ -568,29 +551,29 @@ wheels = [ [[package]] name = "boto3" -version = "1.42.78" +version = "1.42.83" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/2b/ebdad075934cf6bb78bf81fe31d83339bcd804ad6c856f7341376cbc88b6/boto3-1.42.78.tar.gz", hash = "sha256:cef2ebdb9be5c0e96822f8d3941ac4b816c90a5737a7ffb901d664c808964b63", size = 112789, upload-time = "2026-03-27T19:28:07.58Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/87/1ed88eaa1e814841a37e71fee74c2b74341d14b791c0c6038b7ba914bea1/boto3-1.42.83.tar.gz", hash = "sha256:cc5621e603982cb3145b7f6c9970e02e297a1a0eb94637cc7f7b69d3017640ee", size = 112719, upload-time = "2026-04-03T19:34:21.254Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/bb/1f6dade1f1e86858bef7bd332bc8106c445f2dbabec7b32ab5d7d118c9b6/boto3-1.42.78-py3-none-any.whl", hash = "sha256:480a34a077484a5ca60124dfd150ba3ea6517fc89963a679e45b30c6db614d26", size = 140556, upload-time = "2026-03-27T19:28:06.125Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b1/8a066bc8f02937d49783c0b3948ab951d8284e6fde436cab9f359dbd4d93/boto3-1.42.83-py3-none-any.whl", hash = "sha256:544846fdb10585bb7837e409868e8e04c6b372fa04479ba1597ce82cf1242076", size = 140555, upload-time = "2026-04-03T19:34:17.935Z" }, ] [[package]] name = "boto3-stubs" -version = "1.42.78" +version = "1.42.83" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore-stubs" }, { name = "types-s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/03/16/4bdb3c1f69bf7b97dd8b22fe5b007e9da67ba3f00ed10e47146f5fd9d0ff/boto3_stubs-1.42.78.tar.gz", hash = "sha256:423335b8ce9a935e404054978589cdb98d9fa1d4bd46073d6821bf1c3fad8ca7", size = 101602, upload-time = "2026-03-27T19:35:51.149Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/fe/6c43a048074d8567db38befe51bf0b770e8456aa2b91ce8fe6758f29ec3d/boto3_stubs-1.42.83.tar.gz", hash = "sha256:1ecbd88f4ae35764b9ea3579ca1e851b67ea0a73a442cb406de277fc1478daeb", size = 102188, upload-time = "2026-04-03T19:54:20.613Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/d5/bdedd4951c795899ac5a1f0b88d81b9e2c6333cb87457f2edd11ef3b7b7b/boto3_stubs-1.42.78-py3-none-any.whl", hash = "sha256:6ed07e734174751da8d01031d9ede8d81a88e4338d9e6b00ce7a6bc870075372", size = 70161, upload-time = "2026-03-27T19:35:46.336Z" }, + { url = "https://files.pythonhosted.org/packages/9c/4d/eee0444fd466ebe69fdb61cc1f24b97d8e21e9e545865f7c1d846294a413/boto3_stubs-1.42.83-py3-none-any.whl", hash = "sha256:06185ca5f11a1edc880286f5f33779a2b08be356bf270bf1ec128d0819782a20", size = 70448, upload-time = "2026-04-03T19:54:16.315Z" }, ] [package.optional-dependencies] @@ -600,16 +583,16 @@ bedrock-runtime = [ [[package]] name = "botocore" -version = "1.42.78" +version = "1.42.83" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/8e/cdb34c8ca71216d214e049ada2148ee08bcda12b1ac72af3a720dea300ff/botocore-1.42.78.tar.gz", hash = "sha256:61cbd49728e23f68cfd945406ab40044d49abed143362f7ffa4a4f4bd4311791", size = 15023592, upload-time = "2026-03-27T19:27:57.122Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/01/b46a3f8b6e9362258f78f1890db1a96d4ed73214d6a36420dc158dcfd221/botocore-1.42.83.tar.gz", hash = "sha256:34bc8cb64b17ac17f8901f073fe4fc9572a5cac9393a37b2b3ea372a83b87f4a", size = 15140337, upload-time = "2026-04-03T19:34:08.779Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/72/94bba1a375d45c685b00e051b56142359547837086a83861d76f6aec26f4/botocore-1.42.78-py3-none-any.whl", hash = "sha256:038ab63c7f898e8b5db58cb6a45e4da56c31dd984e7e995839a3540c735564ea", size = 14701729, upload-time = "2026-03-27T19:27:54.05Z" }, + { url = "https://files.pythonhosted.org/packages/a3/97/0d6f50822dc8c1df7f3eadb0bc6822fc0f98f02287c4efc7c7c88fde129a/botocore-1.42.83-py3-none-any.whl", hash = "sha256:ec0c3ecb3772936ed22a3bdda09883b34858933f71004686d460d829bab39d8e", size = 14818388, upload-time = "2026-04-03T19:34:03.333Z" }, ] [[package]] @@ -944,7 +927,7 @@ wheels = [ [[package]] name = "clickhouse-connect" -version = "0.15.0" +version = "0.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -953,16 +936,16 @@ dependencies = [ { name = "urllib3" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/59/c0b0a2c2e4c204e5baeca4917a95cc95add651da3cec86ec464a8e54cfa0/clickhouse_connect-0.15.0.tar.gz", hash = "sha256:529fcf072df335d18ae16339d99389190f4bd543067dcdc174541c7a9c622ef5", size = 126344, upload-time = "2026-03-26T18:34:52.316Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/b1/a17eb4409e2741286ccdac06b6ea15db178cdf1f0ed997bbf9ad3448f78e/clickhouse_connect-0.15.1.tar.gz", hash = "sha256:f2aaf5fc0bb3098c24f0d8ca7e4ecbe605a26957481dfca2c8cef9d1fad7b7ca", size = 126840, upload-time = "2026-03-30T18:58:31.113Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/be/86e149c60822caed29e4435acac4fc73e20fddfb0b56ea6452bc7a08ab10/clickhouse_connect-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d51f49694e9007564bfd8dac51a1f9e60b94d6c93a07eb4027113a2e62bbb384", size = 286680, upload-time = "2026-03-26T18:33:30.219Z" }, - { url = "https://files.pythonhosted.org/packages/aa/65/c38cc5028afa2ccd9e8ff65611434063c0c5c1b6edadc507dbbc80a09bfd/clickhouse_connect-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a48fbad9ebc2b6d1cd01d1f9b5d6740081f1c84f1aacc9f91651be949f6b6ed", size = 277579, upload-time = "2026-03-26T18:33:31.474Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ef/c8b2ef597fefd04e8b7c017c991552162cb89b7cb73bfdd6225b1c79e2fe/clickhouse_connect-0.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36e1ae470b94cc56d270461c8626c8fd4dac16e6c1ffa8477f21c012462e22cf", size = 1121630, upload-time = "2026-03-26T18:33:32.983Z" }, - { url = "https://files.pythonhosted.org/packages/de/f7/1b71819e825d44582c014a489618170b03ccdac3c9b710dfd56445f1c017/clickhouse_connect-0.15.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fa97f0ae8eb069a451d8577342dffeef5dc308a0eac7dba1809008c761e720c7", size = 1137988, upload-time = "2026-03-26T18:33:34.585Z" }, - { url = "https://files.pythonhosted.org/packages/7f/1f/41002b8d5ff146dc2835dc6b6f690bc361bd9a94b6195872abcb922f3788/clickhouse_connect-0.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b5b3baf70009174a4df9c8356c96d03e1c2dbf0d8b29f1b3270a641a59399b61", size = 1101376, upload-time = "2026-03-26T18:33:36.258Z" }, - { url = "https://files.pythonhosted.org/packages/2c/8a/bd090dab73fc9c47efcaaeb152a77610b9d233cd88ea73cf4535f9bac2a6/clickhouse_connect-0.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af3fba93fd2efa8f856f3a88a6a710e06005fa48b6b6b0f116d462a4021957e2", size = 1133211, upload-time = "2026-03-26T18:33:38.003Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8d/cf4eee7225bdee85a9b8a88c5bfff42ce48f37ee9277930ac8bc76f47126/clickhouse_connect-0.15.0-cp312-cp312-win32.whl", hash = "sha256:86ca76f8acaf7f3f6530e3e4139e174d54c4674910c69f4277d1b9cdf7c1cc98", size = 256767, upload-time = "2026-03-26T18:33:39.55Z" }, - { url = "https://files.pythonhosted.org/packages/26/6e/f5a2cb1e4624dfd77c1e226239360a9e3690db8056a0027bda2ab87d0085/clickhouse_connect-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:5a471d9a9cf06f0a4e90784547b6a2acb066b0d8642dfea9866960c4bdde6959", size = 275404, upload-time = "2026-03-26T18:33:40.885Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b6/d0881ac34617b13ad555a4749aae042e0242bedbf8a258373719089885cd/clickhouse_connect-0.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0bef871fb9803ae82b4dc1f797b6e784de0a4dec351591191a0c1a6008548284", size = 287187, upload-time = "2026-03-30T18:57:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6e/27823c38e54247ea22d96b3f4fde32831a10e5203761c0e2893bc2fc587f/clickhouse_connect-0.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:df93fa024d6ed46dbc3182b6202180be4cf2bbe9c331dcb21f85963b1b3fd1e5", size = 278086, upload-time = "2026-03-30T18:57:20.104Z" }, + { url = "https://files.pythonhosted.org/packages/6a/88/f1096e8b4f08e628674490e5d186c7bf09174bbbc5fefa530e28e6b39da3/clickhouse_connect-0.15.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6e98c0cf53db3b24dc0ff9f522fcf13205b1d191c632567d1744fbd4671741f", size = 1122144, upload-time = "2026-03-30T18:57:21.205Z" }, + { url = "https://files.pythonhosted.org/packages/af/e5/027f8b94b54a39dcdf9b314a7cd66cb882d8ba166efc584908997c6d5acb/clickhouse_connect-0.15.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bf70933ab860bd2f0a872db624603706bed400c915c7aeef382956cf8ebbdf3", size = 1138503, upload-time = "2026-03-30T18:57:22.554Z" }, + { url = "https://files.pythonhosted.org/packages/cb/46/a830bcb46f0081630a88cb932c29804553728645c17fd1cff874fe71b1ba/clickhouse_connect-0.15.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60aa8c9c775d22db324260265f4c656f803fbc71de9193ef83cf8d8d0ef6ab9a", size = 1101890, upload-time = "2026-03-30T18:57:23.788Z" }, + { url = "https://files.pythonhosted.org/packages/4c/05/91cf7cc817ff91bc96f1e2afc84346b42e88831c9c0a7fd56e78907b5320/clickhouse_connect-0.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5462bad97d97919a4ed230e2ef28d0b76bec0354a343218647830aac7744a43b", size = 1133723, upload-time = "2026-03-30T18:57:25.105Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b0/e7a71b96b7bc1df6bbacf9fa71f0cc3b8f195f58386535b72aa92304b1fb/clickhouse_connect-0.15.1-cp312-cp312-win32.whl", hash = "sha256:e1a157205efd47884c22bfe061fc6f8c9aea844929ee755c47b446093805d21a", size = 257279, upload-time = "2026-03-30T18:57:26.288Z" }, + { url = "https://files.pythonhosted.org/packages/b9/03/0ef116ef0efc6861d6e9674419709b9873603f330f95853220a145748576/clickhouse_connect-0.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:5de299ada0f7eb9090bb5a6304d8d78163d4d9cc8eb04d8f552bfb82bafb61d5", size = 275916, upload-time = "2026-03-30T18:57:27.372Z" }, ] [[package]] @@ -1068,16 +1051,19 @@ wheels = [ [[package]] name = "couchbase" -version = "4.5.0" +version = "4.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/2f/8f92e743a91c2f4e2ebad0bcfc31ef386c817c64415d89bf44e64dde227a/couchbase-4.5.0.tar.gz", hash = "sha256:fb74386ea5e807ae12cfa294fa6740fe6be3ecaf3bb9ce4fb9ea73706ed05982", size = 6562752, upload-time = "2025-09-30T01:27:37.423Z" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/be/1e6974158348dfa634ebbc32b76448f84945e15494852e0cea85607825b5/couchbase-4.6.0.tar.gz", hash = "sha256:61229d6112597f35f6aca687c255e12f495bde9051cd36063b4fddd532ab8f7f", size = 6697937, upload-time = "2026-03-31T23:29:50.602Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/bc/3b00403edd8b188a93f48b8231dbf7faf7b40d318d3e73bb0e68c4965bbd/couchbase-4.5.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:be1ac2bf7cbccf28eebd7fa8b1d7199fbe84c96b0f7f2c0d69963b1d6ce53985", size = 5128307, upload-time = "2025-09-30T01:25:53.615Z" }, - { url = "https://files.pythonhosted.org/packages/7f/52/2ccfa8c8650cc341813713a47eeeb8ad13a25e25b0f4747d224106602a24/couchbase-4.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:035c394d38297c484bd57fc92b27f6a571a36ab5675b4ec873fd15bf65e8f28e", size = 4326149, upload-time = "2025-09-30T01:25:57.524Z" }, - { url = "https://files.pythonhosted.org/packages/32/80/fe3f074f321474c824ec67b97c5c4aa99047d45c777bb29353f9397c6604/couchbase-4.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:117685f6827abbc332e151625b0a9890c2fafe0d3c3d9e564b903d5c411abe5d", size = 5184623, upload-time = "2025-09-30T01:26:02.166Z" }, - { url = "https://files.pythonhosted.org/packages/f3/e5/86381f49e4cf1c6db23c397b6a32b532cd4df7b9975b0cd2da3db2ffe269/couchbase-4.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:632a918f81a7373832991b79b6ab429e56ef4ff68dfb3517af03f0e2be7e3e4f", size = 5446579, upload-time = "2025-09-30T01:26:09.39Z" }, - { url = "https://files.pythonhosted.org/packages/c8/85/a68d04233a279e419062ceb1c6866b61852c016d1854cd09cde7f00bc53c/couchbase-4.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:67fc0fd1a4535b5be093f834116a70fb6609085399e6b63539241b919da737b7", size = 6104619, upload-time = "2025-09-30T01:26:15.525Z" }, - { url = "https://files.pythonhosted.org/packages/56/8c/0511bac5dd2d998aeabcfba6a2804ecd9eb3d83f9d21cc3293a56fbc70a8/couchbase-4.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:02199b4528f3106c231c00aaf85b7cc6723accbc654b903bb2027f78a04d12f4", size = 4274424, upload-time = "2025-09-30T01:26:21.484Z" }, + { url = "https://files.pythonhosted.org/packages/84/dc/bea38235bfabd4fcf3d11e05955e38311869f173328475c369199a6b076b/couchbase-4.6.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:8d1244fd0581cc23aaf2fa3148e9c2d8cfba1d5489c123ee6bf975624d861f7a", size = 5521692, upload-time = "2026-03-31T23:29:07.933Z" }, + { url = "https://files.pythonhosted.org/packages/d1/18/cd1c751005cb67d3e2b090cd11626b8922b9d6a882516e57c1a3aedeed18/couchbase-4.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8efa57a86e35ceb7ae249cfa192e3f2c32a4a5b37098830196d3936994d55a67", size = 4667116, upload-time = "2026-03-31T23:29:10.706Z" }, + { url = "https://files.pythonhosted.org/packages/64/e9/1212bd59347e1cecdb02c6735704650e25f9195b634bf8df73d3382ffa14/couchbase-4.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7106e334acdacab64ae3530a181b8fabf0a1b91e7a1a1e41e259f995bdc78330", size = 5511873, upload-time = "2026-03-31T23:29:13.414Z" }, + { url = "https://files.pythonhosted.org/packages/86/a3/f676ee10f8ea2370700c1c4d03cbe8c3064a3e0cf887941a39333f3bdd97/couchbase-4.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c84e625f3e2ac895fafd2053fa50af2fbb63ab3cdd812eff2bc4171d9f934bde", size = 5782875, upload-time = "2026-03-31T23:29:16.258Z" }, + { url = "https://files.pythonhosted.org/packages/c5/34/45d167bc18d5d91b9ff95dcd4e24df60d424567611d48191a29bf19fdbc8/couchbase-4.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2619c966b308948900e51f1e4e1488e09ad50b119b1d5c31b697870aa82a6ce", size = 7234591, upload-time = "2026-03-31T23:29:19.148Z" }, + { url = "https://files.pythonhosted.org/packages/41/1f/cc4d1503463cf243959532424a30e79f34aadafde5bcb21754b19b2b9dde/couchbase-4.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:f64a017416958f10a07312a6d39c9b362827854de173fdef9bffdac71c8f3345", size = 4517477, upload-time = "2026-03-31T23:29:21.955Z" }, ] [[package]] @@ -1490,7 +1476,7 @@ requires-dist = [ { name = "azure-identity", specifier = "==1.25.3" }, { name = "beautifulsoup4", specifier = "==4.14.3" }, { name = "bleach", specifier = "~=6.3.0" }, - { name = "boto3", specifier = "==1.42.78" }, + { name = "boto3", specifier = "==1.42.83" }, { name = "bs4", specifier = "~=0.0.1" }, { name = "cachetools", specifier = "~=5.3.0" }, { name = "celery", specifier = "~=5.6.2" }, @@ -1498,7 +1484,7 @@ requires-dist = [ { name = "croniter", specifier = ">=6.0.0" }, { name = "fastopenapi", extras = ["flask"], specifier = ">=0.7.0" }, { name = "flask", specifier = "~=3.1.2" }, - { name = "flask-compress", specifier = ">=1.17,<1.24" }, + { name = "flask-compress", specifier = ">=1.17,<1.25" }, { name = "flask-cors", specifier = "~=6.0.0" }, { name = "flask-login", specifier = "~=0.6.3" }, { name = "flask-migrate", specifier = "~=4.1.0" }, @@ -1510,7 +1496,7 @@ requires-dist = [ { name = "google-api-core", specifier = ">=2.19.1" }, { name = "google-api-python-client", specifier = "==2.193.0" }, { name = "google-auth", specifier = ">=2.47.0" }, - { name = "google-auth-httplib2", specifier = "==0.3.0" }, + { name = "google-auth-httplib2", specifier = "==0.3.1" }, { name = "google-cloud-aiplatform", specifier = ">=1.123.0" }, { name = "googleapis-common-protos", specifier = ">=1.65.0" }, { name = "graphon", specifier = ">=0.1.2" }, @@ -1575,18 +1561,18 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "basedpyright", specifier = "~=1.38.2" }, + { name = "basedpyright", specifier = "~=1.39.0" }, { name = "boto3-stubs", specifier = ">=1.38.20" }, { name = "celery-types", specifier = ">=0.23.0" }, { name = "coverage", specifier = "~=7.13.4" }, { name = "dotenv-linter", specifier = "~=0.7.0" }, - { name = "faker", specifier = "~=40.11.0" }, + { name = "faker", specifier = "~=40.12.0" }, { name = "hypothesis", specifier = ">=6.131.15" }, { name = "import-linter", specifier = ">=2.3" }, { name = "lxml-stubs", specifier = "~=0.5.1" }, - { name = "mypy", specifier = "~=1.19.1" }, + { name = "mypy", specifier = "~=1.20.0" }, { name = "pandas-stubs", specifier = "~=3.0.0" }, - { name = "pyrefly", specifier = ">=0.57.1" }, + { name = "pyrefly", specifier = ">=0.59.1" }, { name = "pytest", specifier = "~=9.0.2" }, { name = "pytest-benchmark", specifier = "~=5.2.3" }, { name = "pytest-cov", specifier = "~=7.1.0" }, @@ -1618,10 +1604,10 @@ dev = [ { name = "types-olefile", specifier = "~=0.47.0" }, { name = "types-openpyxl", specifier = "~=3.1.5" }, { name = "types-pexpect", specifier = "~=4.9.0" }, - { name = "types-protobuf", specifier = "~=6.32.1" }, + { name = "types-protobuf", specifier = "~=7.34.1" }, { name = "types-psutil", specifier = "~=7.2.2" }, { name = "types-psycopg2", specifier = "~=2.9.21" }, - { name = "types-pygments", specifier = "~=2.19.0" }, + { name = "types-pygments", specifier = "~=2.20.0" }, { name = "types-pymysql", specifier = "~=1.1.0" }, { name = "types-pyopenssl", specifier = ">=24.1.0" }, { name = "types-python-dateutil", specifier = "~=2.9.0" }, @@ -1629,7 +1615,7 @@ dev = [ { name = "types-pywin32", specifier = "~=311.0.0" }, { name = "types-pyyaml", specifier = "~=6.0.12" }, { name = "types-redis", specifier = ">=4.6.0.20241004" }, - { name = "types-regex", specifier = "~=2026.3.32" }, + { name = "types-regex", specifier = "~=2026.4.4" }, { name = "types-setuptools", specifier = ">=80.9.0" }, { name = "types-shapely", specifier = "~=2.1.0" }, { name = "types-simplejson", specifier = ">=3.20.0" }, @@ -1654,12 +1640,12 @@ tools = [ { name = "nltk", specifier = "~=3.9.1" }, ] vdb = [ - { name = "alibabacloud-gpdb20160503", specifier = "~=5.1.0" }, + { name = "alibabacloud-gpdb20160503", specifier = "~=5.2.0" }, { name = "alibabacloud-tea-openapi", specifier = "~=0.4.3" }, { name = "chromadb", specifier = "==0.5.20" }, { name = "clickhouse-connect", specifier = "~=0.15.0" }, { name = "clickzetta-connector-python", specifier = ">=0.8.102" }, - { name = "couchbase", specifier = "~=4.5.0" }, + { name = "couchbase", specifier = "~=4.6.0" }, { name = "elasticsearch", specifier = "==8.14.0" }, { name = "holo-search-sdk", specifier = ">=0.4.1" }, { name = "intersystems-irispython", specifier = ">=5.1.0" }, @@ -1670,10 +1656,10 @@ vdb = [ { name = "pgvecto-rs", extras = ["sqlalchemy"], specifier = "~=0.2.1" }, { name = "pgvector", specifier = "==0.4.2" }, { name = "pymilvus", specifier = "~=2.6.10" }, - { name = "pymochow", specifier = "==2.3.6" }, + { name = "pymochow", specifier = "==2.4.0" }, { name = "pyobvector", specifier = "~=0.2.17" }, { name = "qdrant-client", specifier = "==1.9.0" }, - { name = "tablestore", specifier = "==6.4.2" }, + { name = "tablestore", specifier = "==6.4.3" }, { name = "tcvectordb", specifier = "~=2.1.0" }, { name = "tidb-vector", specifier = "==0.0.15" }, { name = "upstash-vector", specifier = "==0.8.0" }, @@ -1842,14 +1828,14 @@ wheels = [ [[package]] name = "faker" -version = "40.11.1" +version = "40.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fa/e5/b16bf568a2f20fe7423282db4a4059dbcadef70e9029c1c106836f8edd84/faker-40.11.1.tar.gz", hash = "sha256:61965046e79e8cfde4337d243eac04c0d31481a7c010033141103b43f603100c", size = 1957415, upload-time = "2026-03-23T14:05:50.233Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/c1/f8224fe97fea2f98d455c22438c1b09b10e14ef2cb95ae4f7cec9aa59659/faker-40.12.0.tar.gz", hash = "sha256:58b5a9054c367bd5fb2e948634105364cc570e78a98a8e5161a74691c45f158f", size = 1962003, upload-time = "2026-03-30T18:00:56.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/ec/3c4b78eb0d2f6a81fb8cc9286745845bff661e6815741eff7a6ac5fcc9ea/faker-40.11.1-py3-none-any.whl", hash = "sha256:3af3a213ba8fb33ce6ba2af7aef2ac91363dae35d0cec0b2b0337d189e5bee2a", size = 1989484, upload-time = "2026-03-23T14:05:48.793Z" }, + { url = "https://files.pythonhosted.org/packages/2b/5c/39452a6b6aa76ffa518fa7308e1975b37e9ba77caa6172a69d61e7180221/faker-40.12.0-py3-none-any.whl", hash = "sha256:6238a4058a8b581892e3d78fe5fdfa7568739e1c8283e4ede83f1dde0bfc1a3b", size = 1994601, upload-time = "2026-03-30T18:00:54.804Z" }, ] [[package]] @@ -1950,7 +1936,7 @@ wheels = [ [[package]] name = "flask-compress" -version = "1.23" +version = "1.24" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backports-zstd" }, @@ -1958,9 +1944,9 @@ dependencies = [ { name = "brotlicffi", marker = "platform_python_implementation == 'PyPy'" }, { name = "flask" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5d/e4/2b54da5cf8ae5d38a495ca20154aa40d6d2ee6dc1756429a82856181aa2c/flask_compress-1.23.tar.gz", hash = "sha256:5580935b422e3f136b9a90909e4b1015ac2b29c9aebe0f8733b790fde461c545", size = 20135, upload-time = "2025-11-06T09:06:29.56Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/de/2ae0118051b38ab53437328074a696f3ee7d61e15bf7454b78a3088e5bc3/flask_compress-1.24.tar.gz", hash = "sha256:14097cefe59ecb3e466d52a6aeb62f34f125a9f7dadf1f33a53e430ce4a50f31", size = 21089, upload-time = "2026-03-31T15:01:39.005Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/9a/bebdcdba82d2786b33cd9f5fd65b8d309797c27176a9c4f357c1150c4ac0/flask_compress-1.23-py3-none-any.whl", hash = "sha256:52108afb4d133a5aab9809e6ac3c085ed7b9c788c75c6846c129faa28468f08c", size = 10515, upload-time = "2025-11-06T09:06:28.691Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0f/fe51e0b2301bbd429af44273a923ff92127b18d13abba5ae5a1d60e8e497/flask_compress-1.24-py3-none-any.whl", hash = "sha256:1e63668eb6e3242bd4f6ad98825a924e3984409be90c125477893d586007d00c", size = 11033, upload-time = "2026-03-31T15:01:37.302Z" }, ] [[package]] @@ -2174,7 +2160,7 @@ wheels = [ [[package]] name = "google-api-core" -version = "2.30.0" +version = "2.30.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth" }, @@ -2183,9 +2169,9 @@ dependencies = [ { name = "protobuf" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/98/586ec94553b569080caef635f98a3723db36a38eac0e3d7eb3ea9d2e4b9a/google_api_core-2.30.0.tar.gz", hash = "sha256:02edfa9fab31e17fc0befb5f161b3bf93c9096d99aed584625f38065c511ad9b", size = 176959, upload-time = "2026-02-18T20:28:11.926Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/2e/83ca41eb400eb228f9279ec14ed66f6475218b59af4c6daec2d5a509fe83/google_api_core-2.30.2.tar.gz", hash = "sha256:9a8113e1a88bdc09a7ff629707f2214d98d61c7f6ceb0ea38c42a095d02dc0f9", size = 176862, upload-time = "2026-04-02T21:23:44.876Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/27/09c33d67f7e0dcf06d7ac17d196594e66989299374bfb0d4331d1038e76b/google_api_core-2.30.0-py3-none-any.whl", hash = "sha256:80be49ee937ff9aba0fd79a6eddfde35fe658b9953ab9b79c57dd7061afa8df5", size = 173288, upload-time = "2026-02-18T20:28:10.367Z" }, + { url = "https://files.pythonhosted.org/packages/84/e1/ebd5100cbb202e561c0c8b59e485ef3bd63fa9beb610f3fdcaea443f0288/google_api_core-2.30.2-py3-none-any.whl", hash = "sha256:a4c226766d6af2580577db1f1a51bf53cd262f722b49731ce7414c43068a9594", size = 173236, upload-time = "2026-04-02T21:23:06.395Z" }, ] [package.optional-dependencies] @@ -2230,20 +2216,20 @@ requests = [ [[package]] name = "google-auth-httplib2" -version = "0.3.0" +version = "0.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth" }, { name = "httplib2" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d5/ad/c1f2b1175096a8d04cf202ad5ea6065f108d26be6fc7215876bde4a7981d/google_auth_httplib2-0.3.0.tar.gz", hash = "sha256:177898a0175252480d5ed916aeea183c2df87c1f9c26705d74ae6b951c268b0b", size = 11134, upload-time = "2025-12-15T22:13:51.825Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/99/107612bef8d24b298bb5a7c8466f908ecda791d43f9466f5c3978f5b24c1/google_auth_httplib2-0.3.1.tar.gz", hash = "sha256:0af542e815784cb64159b4469aa5d71dd41069ba93effa006e1916b1dcd88e55", size = 11152, upload-time = "2026-03-30T22:50:26.766Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/d5/3c97526c8796d3caf5f4b3bed2b05e8a7102326f00a334e7a438237f3b22/google_auth_httplib2-0.3.0-py3-none-any.whl", hash = "sha256:426167e5df066e3f5a0fc7ea18768c08e7296046594ce4c8c409c2457dd1f776", size = 9529, upload-time = "2025-12-15T22:13:51.048Z" }, + { url = "https://files.pythonhosted.org/packages/97/e9/93afb14d23a949acaa3f4e7cc51a0024671174e116e35f42850764b99634/google_auth_httplib2-0.3.1-py3-none-any.whl", hash = "sha256:682356a90ef4ba3d06548c37e9112eea6fc00395a11b0303a644c1a86abc275c", size = 9534, upload-time = "2026-03-30T22:49:03.384Z" }, ] [[package]] name = "google-cloud-aiplatform" -version = "1.143.0" +version = "1.145.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docstring-parser" }, @@ -2259,9 +2245,9 @@ dependencies = [ { name = "pydantic" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/08/939fb05870fdf155410a927e22f5b053d49f18e215618e102fba1d8bb147/google_cloud_aiplatform-1.143.0.tar.gz", hash = "sha256:1f0124a89795a6b473deb28724dd37d95334205df3a9c9c48d0b8d7a3d5d5cc4", size = 10215389, upload-time = "2026-03-25T18:30:15.444Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/e5/6442d9d2c019456638825d4665b1e87ec4eaf1d182950ba426d0f0210eab/google_cloud_aiplatform-1.145.0.tar.gz", hash = "sha256:7894c4f3d2684bdb60e9a122004c01678e3b585174a27298ae7a3ed1e5eaf3bd", size = 10222904, upload-time = "2026-04-02T14:06:58.322Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/14/16323e604e79dc63b528268f97a841c2c29dd8eb16395de6bf530c1a5ebe/google_cloud_aiplatform-1.143.0-py2.py3-none-any.whl", hash = "sha256:78df97d044859f743a9cc48b89a260d33579b0d548b1589bb3ae9f4c2afc0c5a", size = 8392705, upload-time = "2026-03-25T18:30:11.496Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/23e98d3407d5e2416a3dfaecb0a053da899848c50db69e5f2b61a555ce06/google_cloud_aiplatform-1.145.0-py2.py3-none-any.whl", hash = "sha256:4d1c31797a8bd8f3342ed5f186dd30d1f6bca73ddbee2bde452777100d2ddc11", size = 8396640, upload-time = "2026-04-02T14:06:54.125Z" }, ] [[package]] @@ -2377,14 +2363,14 @@ wheels = [ [[package]] name = "googleapis-common-protos" -version = "1.73.1" +version = "1.74.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/c0/4a54c386282c13449eca8bbe2ddb518181dc113e78d240458a68856b4d69/googleapis_common_protos-1.73.1.tar.gz", hash = "sha256:13114f0e9d2391756a0194c3a8131974ed7bffb06086569ba193364af59163b6", size = 147506, upload-time = "2026-03-26T22:17:38.451Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/18/a746c8344152d368a5aac738d4c857012f2c5d1fd2eac7e17b647a7861bd/googleapis_common_protos-1.74.0.tar.gz", hash = "sha256:57971e4eeeba6aad1163c1f0fc88543f965bb49129b8bb55b2b7b26ecab084f1", size = 151254, upload-time = "2026-04-02T21:23:26.679Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/82/fcb6520612bec0c39b973a6c0954b6a0d948aadfe8f7e9487f60ceb8bfa6/googleapis_common_protos-1.73.1-py3-none-any.whl", hash = "sha256:e51f09eb0a43a8602f5a915870972e6b4a394088415c79d79605a46d8e826ee8", size = 297556, upload-time = "2026-03-26T22:15:58.455Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b0/be5d3329badb9230b765de6eea66b73abd5944bdeb5afb3562ddcd80ae84/googleapis_common_protos-1.74.0-py3-none-any.whl", hash = "sha256:702216f78610bb510e3f12ac3cafd281b7ac45cc5d86e90ad87e4d301a3426b5", size = 300743, upload-time = "2026-04-02T21:22:49.108Z" }, ] [package.optional-dependencies] @@ -2800,14 +2786,14 @@ wheels = [ [[package]] name = "hypothesis" -version = "6.151.10" +version = "6.151.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/dd/633e2cd62377333b7681628aee2ec1d88166f5bdf916b08c98b1e8288ad3/hypothesis-6.151.10.tar.gz", hash = "sha256:6c9565af8b4aa3a080b508f66ce9c2a77dd613c7e9073e27fc7e4ef9f45f8a27", size = 463762, upload-time = "2026-03-29T01:06:22.19Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/58/41af0d539b3c95644d1e4e353cbd6ac9473e892ea21802546a8886b79078/hypothesis-6.151.11.tar.gz", hash = "sha256:f33dcb68b62c7b07c9ac49664989be898fa8ce57583f0dc080259a197c6c7ff1", size = 463779, upload-time = "2026-04-05T17:35:55.935Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/da/439bb2e451979f5e88c13bbebc3e9e17754429cfb528c93677b2bd81783b/hypothesis-6.151.10-py3-none-any.whl", hash = "sha256:b0d7728f0c8c2be009f89fcdd6066f70c5439aa0f94adbb06e98261d05f49b05", size = 529493, upload-time = "2026-03-29T01:06:19.161Z" }, + { url = "https://files.pythonhosted.org/packages/1d/06/f49393eca84b87b17a67aaebf9f6251190ba1e9fe9f2236504049fc43fee/hypothesis-6.151.11-py3-none-any.whl", hash = "sha256:7ac05173206746cec8312f95164a30a4eb4916815413a278922e63ff1e404648", size = 529572, upload-time = "2026-04-05T17:35:53.438Z" }, ] [[package]] @@ -2875,14 +2861,14 @@ wheels = [ [[package]] name = "intersystems-irispython" -version = "5.3.1" +version = "5.3.2" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/5b/8eac672a6ef26bef6ef79a7c9557096167b50c4d3577d558ae6999c195fe/intersystems_irispython-5.3.1-cp38.cp39.cp310.cp311.cp312.cp313.cp314-cp38.cp39.cp310.cp311.cp312.cp313.cp314-macosx_10_9_universal2.whl", hash = "sha256:634c9b4ec620837d830ff49543aeb2797a1ce8d8570a0e868398b85330dfcc4d", size = 6736686, upload-time = "2025-12-19T16:24:57.734Z" }, - { url = "https://files.pythonhosted.org/packages/ba/17/bab3e525ffb6711355f7feea18c1b7dced9c2484cecbcdd83f74550398c0/intersystems_irispython-5.3.1-cp38.cp39.cp310.cp311.cp312.cp313.cp314-cp38.cp39.cp310.cp311.cp312.cp313.cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cf912f30f85e2a42f2c2ea77fbeb98a24154d5ea7428a50382786a684ec4f583", size = 16005259, upload-time = "2025-12-19T16:25:05.578Z" }, - { url = "https://files.pythonhosted.org/packages/39/59/9bb79d9e32e3e55fc9aed8071a797b4497924cbc6457cea9255bb09320b7/intersystems_irispython-5.3.1-cp38.cp39.cp310.cp311.cp312.cp313.cp314-cp38.cp39.cp310.cp311.cp312.cp313.cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be5659a6bb57593910f2a2417eddb9f5dc2f93a337ead6ddca778f557b8a359a", size = 15638040, upload-time = "2025-12-19T16:24:54.429Z" }, - { url = "https://files.pythonhosted.org/packages/cf/47/654ccf9c5cca4f5491f070888544165c9e2a6a485e320ea703e4e38d2358/intersystems_irispython-5.3.1-cp38.cp39.cp310.cp311.cp312.cp313.cp314-cp38.cp39.cp310.cp311.cp312.cp313.cp314-win32.whl", hash = "sha256:583e4f17088c1e0530f32efda1c0ccb02993cbc22035bc8b4c71d8693b04ee7e", size = 2879644, upload-time = "2025-12-19T16:24:59.945Z" }, - { url = "https://files.pythonhosted.org/packages/68/95/19cc13d09f1b4120bd41b1434509052e1d02afd27f2679266d7ad9cc1750/intersystems_irispython-5.3.1-cp38.cp39.cp310.cp311.cp312.cp313.cp314-cp38.cp39.cp310.cp311.cp312.cp313.cp314-win_amd64.whl", hash = "sha256:1d5d40450a0cdeec2a1f48d12d946a8a8ffc7c128576fcae7d58e66e3a127eae", size = 3522092, upload-time = "2025-12-19T16:25:01.834Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/0a7bc92e68480d523015eb454aa0ec73a33320975d10d5500ba54ccd124e/intersystems_irispython-5.3.2-cp38.cp39.cp310.cp311.cp312.cp313.cp314-cp38.cp39.cp310.cp311.cp312.cp313.cp314-macosx_10_9_universal2.whl", hash = "sha256:8af5e31273ad97c391141111630e8303d510272360b609990a8c85e56a7850ac", size = 7121915, upload-time = "2026-03-31T18:53:12.205Z" }, + { url = "https://files.pythonhosted.org/packages/22/cc/2f066a0dc82fae884b655d2f862bd51dd21a4322d4b9f898117f74c010b4/intersystems_irispython-5.3.2-cp38.cp39.cp310.cp311.cp312.cp313.cp314-cp38.cp39.cp310.cp311.cp312.cp313.cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:25663d3cce7b414451a781ffaeb785e8f8439d0275920ffd4f05add2c056abfd", size = 16247974, upload-time = "2026-03-31T18:53:13.798Z" }, + { url = "https://files.pythonhosted.org/packages/27/cd/cef09a8310541d99fdbe89b2eccc21a6d776384325a9a6e740ad01e8461f/intersystems_irispython-5.3.2-cp38.cp39.cp310.cp311.cp312.cp313.cp314-cp38.cp39.cp310.cp311.cp312.cp313.cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5cb6efc3e2b9651f1c37539a3f69a823e80c32210d11d745cffad1eca4c7995", size = 15900577, upload-time = "2026-03-31T18:53:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/37/91/0e08555834de10f59810ef6c615af72c3f234920c70cc0421d455ba9c359/intersystems_irispython-5.3.2-cp38.cp39.cp310.cp311.cp312.cp313.cp314-cp38.cp39.cp310.cp311.cp312.cp313.cp314-win32.whl", hash = "sha256:a250b21067c9e8275232ca798dcfe0719a970cd6ec9f2023923c810fffa46f41", size = 3046761, upload-time = "2026-03-31T18:53:09.151Z" }, + { url = "https://files.pythonhosted.org/packages/21/28/00b6b03b648005cb9c14dc75943e7cccce83eb5fd8fdba502028c25c7fc4/intersystems_irispython-5.3.2-cp38.cp39.cp310.cp311.cp312.cp313.cp314-cp38.cp39.cp310.cp311.cp312.cp313.cp314-win_amd64.whl", hash = "sha256:43feb7e23bc9f77db7bb140d1b55c22090b0c46691b570b1faaf6875baa6452d", size = 3742519, upload-time = "2026-03-31T18:53:10.597Z" }, ] [[package]] @@ -3066,12 +3052,11 @@ sdist = { url = "https://files.pythonhosted.org/packages/0e/72/a3add0e4eec4eb9e2 [[package]] name = "langfuse" -version = "4.0.1" +version = "4.0.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backoff" }, { name = "httpx" }, - { name = "openai" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-proto-http" }, { name = "opentelemetry-sdk" }, @@ -3079,14 +3064,14 @@ dependencies = [ { name = "pydantic" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/94/ab00e21fa5977d6b9c68fb3a95de2aa1a1e586964ff2af3e37405bf65d9f/langfuse-4.0.1.tar.gz", hash = "sha256:40a6daf3ab505945c314246d5b577d48fcfde0a47e8c05267ea6bd494ae9608e", size = 272749, upload-time = "2026-03-19T14:03:34.508Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/d0/6d79ed5614f86f27f5df199cf10c6facf6874ff6f91b828ae4dad90aa86d/langfuse-4.0.6.tar.gz", hash = "sha256:83a6f8cc8f1431fa2958c91e2673bc4179f993297e9b1acd1dbf001785e6cf83", size = 274094, upload-time = "2026-04-01T20:04:15.153Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/8f/3145ef00940f9c29d7e0200fd040f35616eac21c6ab4610a1ba14f3a04c1/langfuse-4.0.1-py3-none-any.whl", hash = "sha256:e22f49ea31304f97fc31a97c014ba63baa8802d9568295d54f06b00b43c30524", size = 465049, upload-time = "2026-03-19T14:03:32.527Z" }, + { url = "https://files.pythonhosted.org/packages/50/b4/088048e37b6d7ec1b52c6a11bc33101454285a22eaab8303dcccfd78344d/langfuse-4.0.6-py3-none-any.whl", hash = "sha256:0562b1dcf83247f9d8349f0f755eaed9a7f952fee67e66580970f0738bf3adbf", size = 472841, upload-time = "2026-04-01T20:04:16.451Z" }, ] [[package]] name = "langsmith" -version = "0.7.22" +version = "0.7.25" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -3099,9 +3084,9 @@ dependencies = [ { name = "xxhash" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/2a/2d5e6c67396fd228670af278c4da7bd6db2b8d11deaf6f108490b6d3f561/langsmith-0.7.22.tar.gz", hash = "sha256:35bfe795d648b069958280760564632fd28ebc9921c04f3e209c0db6a6c7dc04", size = 1134923, upload-time = "2026-03-19T22:45:23.492Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/d7/21ffae5ccdc3c9b8de283e8f8bf48a92039681df0d39f15133d8ff8965bd/langsmith-0.7.25.tar.gz", hash = "sha256:d17da71f156ca69eafd28ac9627c8e0e93170260ec37cd27cedc83205a067598", size = 1145410, upload-time = "2026-04-03T13:11:42.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/94/1f5d72655ab6534129540843776c40eff757387b88e798d8b3bf7e313fd4/langsmith-0.7.22-py3-none-any.whl", hash = "sha256:6e9d5148314d74e86748cb9d3898632cad0320c9323d95f70f969e5bc078eee4", size = 359927, upload-time = "2026-03-19T22:45:21.603Z" }, + { url = "https://files.pythonhosted.org/packages/29/13/67889d41baf7dbaf13ffd0b334a0f284e107fad1cc8782a1abb1e56e5eeb/langsmith-0.7.25-py3-none-any.whl", hash = "sha256:55ecc24c547f6c79b5a684ff8685c669eec34e52fcac5d2c0af7d613aef5a632", size = 359417, upload-time = "2026-04-03T13:11:40.729Z" }, ] [[package]] @@ -3430,7 +3415,7 @@ wheels = [ [[package]] name = "mypy" -version = "1.19.1" +version = "1.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, @@ -3438,15 +3423,16 @@ dependencies = [ { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/b0089fe7fef0a994ae5ee07029ced0526082c6cfaaa4c10d40a10e33b097/mypy-1.20.0.tar.gz", hash = "sha256:eb96c84efcc33f0b5e0e04beacf00129dd963b67226b01c00b9dfc8affb464c3", size = 3815028, upload-time = "2026-03-31T16:55:14.959Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, - { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, - { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, - { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, - { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, + { url = "https://files.pythonhosted.org/packages/be/dd/3afa29b58c2e57c79116ed55d700721c3c3b15955e2b6251dd165d377c0e/mypy-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:002b613ae19f4ac7d18b7e168ffe1cb9013b37c57f7411984abbd3b817b0a214", size = 14509525, upload-time = "2026-03-31T16:55:01.824Z" }, + { url = "https://files.pythonhosted.org/packages/54/eb/227b516ab8cad9f2a13c5e7a98d28cd6aa75e9c83e82776ae6c1c4c046c7/mypy-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9336b5e6712f4adaf5afc3203a99a40b379049104349d747eb3e5a3aa23ac2e", size = 13326469, upload-time = "2026-03-31T16:51:41.23Z" }, + { url = "https://files.pythonhosted.org/packages/57/d4/1ddb799860c1b5ac6117ec307b965f65deeb47044395ff01ab793248a591/mypy-1.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f13b3e41bce9d257eded794c0f12878af3129d80aacd8a3ee0dee51f3a978651", size = 13705953, upload-time = "2026-03-31T16:48:55.69Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b7/54a720f565a87b893182a2a393370289ae7149e4715859e10e1c05e49154/mypy-1.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9804c3ad27f78e54e58b32e7cb532d128b43dbfb9f3f9f06262b821a0f6bd3f5", size = 14710363, upload-time = "2026-03-31T16:53:26.948Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2a/74810274848d061f8a8ea4ac23aaad43bd3d8c1882457999c2e568341c57/mypy-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:697f102c5c1d526bdd761a69f17c6070f9892eebcb94b1a5963d679288c09e78", size = 14947005, upload-time = "2026-03-31T16:50:17.591Z" }, + { url = "https://files.pythonhosted.org/packages/77/91/21b8ba75f958bcda75690951ce6fa6b7138b03471618959529d74b8544e2/mypy-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ecd63f75fdd30327e4ad8b5704bd6d91fc6c1b2e029f8ee14705e1207212489", size = 10880616, upload-time = "2026-03-31T16:52:19.986Z" }, + { url = "https://files.pythonhosted.org/packages/8a/15/3d8198ef97c1ca03aea010cce4f1d4f3bc5d9849e8c0140111ca2ead9fdd/mypy-1.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:f194db59657c58593a3c47c6dfd7bad4ef4ac12dbc94d01b3a95521f78177e33", size = 9813091, upload-time = "2026-03-31T16:53:44.385Z" }, + { url = "https://files.pythonhosted.org/packages/21/66/4d734961ce167f0fd8380769b3b7c06dbdd6ff54c2190f3f2ecd22528158/mypy-1.20.0-py3-none-any.whl", hash = "sha256:a6e0641147cbfa7e4e94efdb95c2dab1aff8cfc159ded13e07f308ddccc8c48e", size = 2636365, upload-time = "2026-03-31T16:51:44.911Z" }, ] [[package]] @@ -4023,7 +4009,7 @@ wheels = [ [[package]] name = "opik" -version = "1.10.54" +version = "1.10.58" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "boto3-stubs", extra = ["bedrock-runtime"] }, @@ -4042,9 +4028,9 @@ dependencies = [ { name = "tqdm" }, { name = "uuid6" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fa/c9/ecc68c5ae32bf5b1074bdc713cb1543b8e2a46c58c814bf150fecf50f272/opik-1.10.54.tar.gz", hash = "sha256:46e29abf4656bd80b9cb339659d24ecf97b61f37c3fde594de75e5f59953e9d3", size = 812757, upload-time = "2026-03-27T11:23:06.109Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/bc/54673138cf374226ab9fcdd5685e92442c0d5a95775ff22b870c767387e6/opik-1.10.58.tar.gz", hash = "sha256:058f8b3e3171a1f5e75f25cf1fea392b8f2e0ddba18765fafd24cd756783002b", size = 833671, upload-time = "2026-04-01T11:43:21.571Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/91/1ae4e8a349da0620a6f0a4fc51cd00c3e75176939d022e8684379aee2928/opik-1.10.54-py3-none-any.whl", hash = "sha256:5f8ddabe5283ebe08d455e81b188d6e09ce1d1efa989f8b05567ef70f1e9aeda", size = 1379008, upload-time = "2026-03-27T11:23:04.582Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/99cf048209f10f8444544202b007d5fbe0a6104465d29038b25932b1c79f/opik-1.10.58-py3-none-any.whl", hash = "sha256:29be9d7f846f3229a027250997195e583da840179ad03f3d28b1d613687963e3", size = 1400658, upload-time = "2026-04-01T11:43:20.096Z" }, ] [[package]] @@ -4191,11 +4177,11 @@ wheels = [ [[package]] name = "pathspec" -version = "0.12.1" +version = "1.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] @@ -4705,16 +4691,16 @@ wheels = [ [[package]] name = "pymochow" -version = "2.3.6" +version = "2.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "future" }, { name = "orjson" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/04/2edda5447aa7c87a0b2b7c75406cc0fbcceeddd09c76b04edfb84eb47499/pymochow-2.3.6.tar.gz", hash = "sha256:6249a2fa410ef22e9e702710d725e7e052f492af87233ffe911845f931557632", size = 51123, upload-time = "2025-12-12T06:23:24.162Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/06/ba1b9ad8939a7289196df73934eb805bdd3e38473ccf2edcc06018f156c5/pymochow-2.4.0.tar.gz", hash = "sha256:63d9f9abc44d3643b4384fd233005978a0079b45bbb35700a81ccb99c1442cfd", size = 51300, upload-time = "2026-04-02T10:24:11.883Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/86/588c75acbcc7dd9860252f1ef2233212f36b6751ac0cdec15867fc2fc4d6/pymochow-2.3.6-py3-none-any.whl", hash = "sha256:d46cb3af4d908f0c15d875190b1945c0353b907d7e32f068636ee04433cf06b1", size = 78963, upload-time = "2025-12-12T06:23:21.419Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f8/d3c23f0e1d15c66ce3e431cf1866309c375c0685ff0ed6e4ae21f72161b2/pymochow-2.4.0-py3-none-any.whl", hash = "sha256:52d128aa9bea643f51aded91fed99af4d6421922e7696dfe9a1877684469d172", size = 79149, upload-time = "2026-04-02T10:24:10.029Z" }, ] [[package]] @@ -4839,18 +4825,19 @@ wheels = [ [[package]] name = "pyrefly" -version = "0.57.1" +version = "0.59.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/c1/c17211e5bbd2b90a24447484713da7cc2cee4e9455e57b87016ffc69d426/pyrefly-0.57.1.tar.gz", hash = "sha256:b05f6f5ee3a6a5d502ca19d84cb9ab62d67f05083819964a48c1510f2993efc6", size = 5310800, upload-time = "2026-03-18T18:42:35.614Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/ce/7882c2af92b2ff6505fcd3430eff8048ece6c6254cc90bdc76ecee12dfab/pyrefly-0.59.1.tar.gz", hash = "sha256:bf1675b0c38d45df2c8f8618cbdfa261a1b92430d9d31eba16e0282b551e210f", size = 5475432, upload-time = "2026-04-01T22:04:04.11Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/58/8af37856c8d45b365ece635a6728a14b0356b08d1ff1ac601d7120def1e0/pyrefly-0.57.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:91974bfbe951eebf5a7bc959c1f3921f0371c789cad84761511d695e9ab2265f", size = 12681847, upload-time = "2026-03-18T18:42:10.963Z" }, - { url = "https://files.pythonhosted.org/packages/5f/d7/fae6dd9d0355fc5b8df7793f1423b7433ca8e10b698ea934c35f0e4e6522/pyrefly-0.57.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:808087298537c70f5e7cdccb5bbaad482e7e056e947c0adf00fb612cbace9fdc", size = 12219634, upload-time = "2026-03-18T18:42:13.469Z" }, - { url = "https://files.pythonhosted.org/packages/29/8f/9511ae460f0690e837b9ba0f7e5e192079e16ff9a9ba8a272450e81f11f8/pyrefly-0.57.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b01f454fa5539e070c0cba17ddec46b3d2107d571d519bd8eca8f3142ba02a6", size = 34947757, upload-time = "2026-03-18T18:42:17.152Z" }, - { url = "https://files.pythonhosted.org/packages/07/43/f053bf9c65218f70e6a49561e9942c7233f8c3e4da8d42e5fe2aae50b3d2/pyrefly-0.57.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02ad59ea722191f51635f23e37574662116b82ca9d814529f7cb5528f041f381", size = 37621018, upload-time = "2026-03-18T18:42:20.79Z" }, - { url = "https://files.pythonhosted.org/packages/0e/76/9cea46de01665bbc125e4f215340c9365c8d56cda6198ff238a563ea8e75/pyrefly-0.57.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54bc0afe56776145e37733ff763e7e9679ee8a76c467b617dc3f227d4124a9e2", size = 40203649, upload-time = "2026-03-18T18:42:24.519Z" }, - { url = "https://files.pythonhosted.org/packages/fd/8b/2fb4a96d75e2a57df698a43e2970e441ba2704e3906cdc0386a055daa05a/pyrefly-0.57.1-py3-none-win32.whl", hash = "sha256:468e5839144b25bb0dce839bfc5fd879c9f38e68ebf5de561f30bed9ae19d8ca", size = 11732953, upload-time = "2026-03-18T18:42:27.379Z" }, - { url = "https://files.pythonhosted.org/packages/13/5a/4a197910fe2e9b102b15ae5e7687c45b7b5981275a11a564b41e185dd907/pyrefly-0.57.1-py3-none-win_amd64.whl", hash = "sha256:46db9c97093673c4fb7fab96d610e74d140661d54688a92d8e75ad885a56c141", size = 12537319, upload-time = "2026-03-18T18:42:30.196Z" }, - { url = "https://files.pythonhosted.org/packages/b5/c6/bc442874be1d9b63da1f9debb4f04b7d0c590a8dc4091921f3c288207242/pyrefly-0.57.1-py3-none-win_arm64.whl", hash = "sha256:feb1bbe3b0d8d5a70121dcdf1476e6a99cc056a26a49379a156f040729244dcb", size = 12013455, upload-time = "2026-03-18T18:42:32.928Z" }, + { url = "https://files.pythonhosted.org/packages/d0/10/04a0e05b08fc855b6fe38c3df549925fc3c2c6e750506870de7335d3e1f7/pyrefly-0.59.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:390db3cd14aa7e0268e847b60cd9ee18b04273eddfa38cf341ed3bb43f3fef2a", size = 12868133, upload-time = "2026-04-01T22:03:39.436Z" }, + { url = "https://files.pythonhosted.org/packages/c7/78/fa7be227c3e3fcacee501c1562278dd026186ffd1b5b5beb51d3941a3aed/pyrefly-0.59.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d246d417b6187c1650d7f855f61c68fbfd6d6155dc846d4e4d273a3e6b5175cb", size = 12379325, upload-time = "2026-04-01T22:03:42.046Z" }, + { url = "https://files.pythonhosted.org/packages/bb/13/6828ce1c98171b5f8388f33c4b0b9ea2ab8c49abe0ef8d793c31e30a05cb/pyrefly-0.59.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:575ac67b04412dc651a7143d27e38a40fbdd3c831c714d5520d0e9d4c8631ab4", size = 35826408, upload-time = "2026-04-01T22:03:45.067Z" }, + { url = "https://files.pythonhosted.org/packages/23/56/79ed8ece9a7ecad0113c394a06a084107db3ad8f1fefe19e7ded43c51245/pyrefly-0.59.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:062e6262ce1064d59dcad81ac0499bb7a3ad501e9bc8a677a50dc630ff0bf862", size = 38532699, upload-time = "2026-04-01T22:03:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/18/7d/ecc025e0f0e3f295b497f523cc19cefaa39e57abede8fc353d29445d174b/pyrefly-0.59.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ef4247f9e6f734feb93e1f2b75335b943629956e509f545cc9cdcccd76dd20", size = 36743570, upload-time = "2026-04-01T22:03:51.362Z" }, + { url = "https://files.pythonhosted.org/packages/2f/03/b1ce882ebcb87c673165c00451fbe4df17bf96ccfde18c75880dc87c5f5e/pyrefly-0.59.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59a2d01723b84d042f4fa6ec871ffd52d0a7e83b0ea791c2e0bb0ff750abce56", size = 41236246, upload-time = "2026-04-01T22:03:54.361Z" }, + { url = "https://files.pythonhosted.org/packages/17/af/5e9c7afd510e7dd64a2204be0ed39e804089cbc4338675a28615c7176acb/pyrefly-0.59.1-py3-none-win32.whl", hash = "sha256:4ea70c780848f8376411e787643ae5d2d09da8a829362332b7b26d15ebcbaf56", size = 11884747, upload-time = "2026-04-01T22:03:56.776Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c1/7db1077627453fd1068f0761f059a9512645c00c4c20acfb9f0c24ac02ec/pyrefly-0.59.1-py3-none-win_amd64.whl", hash = "sha256:67e6a08cfd129a0d2788d5e40a627f9860e0fe91a876238d93d5c63ff4af68ae", size = 12720608, upload-time = "2026-04-01T22:03:59.252Z" }, + { url = "https://files.pythonhosted.org/packages/07/16/4bb6e5fce5a9cf0992932d9435d964c33e507aaaf96fdfbb1be493078a4a/pyrefly-0.59.1-py3-none-win_arm64.whl", hash = "sha256:01179cb215cf079e8223a064f61a074f7079aa97ea705cbbc68af3d6713afd15", size = 12223158, upload-time = "2026-04-01T22:04:01.869Z" }, ] [[package]] @@ -5323,27 +5310,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.8" +version = "0.15.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size = 4610921, upload-time = "2026-03-26T18:39:38.675Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/97/e9f1ca355108ef7194e38c812ef40ba98c7208f47b13ad78d023caa583da/ruff-0.15.9.tar.gz", hash = "sha256:29cbb1255a9797903f6dde5ba0188c707907ff44a9006eb273b5a17bfa0739a2", size = 4617361, upload-time = "2026-04-02T18:17:20.829Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size = 10527394, upload-time = "2026-03-26T18:39:41.566Z" }, - { url = "https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size = 10905693, upload-time = "2026-03-26T18:39:30.364Z" }, - { url = "https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size = 10323044, upload-time = "2026-03-26T18:39:33.37Z" }, - { url = "https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size = 10629135, upload-time = "2026-03-26T18:39:44.142Z" }, - { url = "https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size = 10348041, upload-time = "2026-03-26T18:39:52.178Z" }, - { url = "https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size = 11121987, upload-time = "2026-03-26T18:39:55.195Z" }, - { url = "https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size = 11951057, upload-time = "2026-03-26T18:39:19.18Z" }, - { url = "https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size = 11464613, upload-time = "2026-03-26T18:40:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size = 11257557, upload-time = "2026-03-26T18:39:57.972Z" }, - { url = "https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size = 11169440, upload-time = "2026-03-26T18:39:22.205Z" }, - { url = "https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size = 10605963, upload-time = "2026-03-26T18:39:46.682Z" }, - { url = "https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size = 10357484, upload-time = "2026-03-26T18:39:49.176Z" }, - { url = "https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size = 10830426, upload-time = "2026-03-26T18:40:03.702Z" }, - { url = "https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size = 11345125, upload-time = "2026-03-26T18:39:27.799Z" }, - { url = "https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size = 10571959, upload-time = "2026-03-26T18:39:36.117Z" }, - { url = "https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size = 11743893, upload-time = "2026-03-26T18:39:25.01Z" }, - { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/9cdfd0ac4b9d1e5a6cf09bedabdf0b56306ab5e333c85c87281273e7b041/ruff-0.15.9-py3-none-linux_armv6l.whl", hash = "sha256:6efbe303983441c51975c243e26dff328aca11f94b70992f35b093c2e71801e1", size = 10511206, upload-time = "2026-04-02T18:16:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f6/32bfe3e9c136b35f02e489778d94384118bb80fd92c6d92e7ccd97db12ce/ruff-0.15.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4965bac6ac9ea86772f4e23587746f0b7a395eccabb823eb8bfacc3fa06069f7", size = 10923307, upload-time = "2026-04-02T18:17:08.645Z" }, + { url = "https://files.pythonhosted.org/packages/ca/25/de55f52ab5535d12e7aaba1de37a84be6179fb20bddcbe71ec091b4a3243/ruff-0.15.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf05aad70ca5b5a0a4b0e080df3a6b699803916d88f006efd1f5b46302daab8", size = 10316722, upload-time = "2026-04-02T18:16:44.206Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/690d75f3fd6278fe55fff7c9eb429c92d207e14b25d1cae4064a32677029/ruff-0.15.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9439a342adb8725f32f92732e2bafb6d5246bd7a5021101166b223d312e8fc59", size = 10623674, upload-time = "2026-04-02T18:16:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ec/176f6987be248fc5404199255522f57af1b4a5a1b57727e942479fec98ad/ruff-0.15.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5e6faf9d97c8edc43877c3f406f47446fc48c40e1442d58cfcdaba2acea745", size = 10351516, upload-time = "2026-04-02T18:16:57.206Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fc/51cffbd2b3f240accc380171d51446a32aa2ea43a40d4a45ada67368fbd2/ruff-0.15.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b34a9766aeec27a222373d0b055722900fbc0582b24f39661aa96f3fe6ad901", size = 11150202, upload-time = "2026-04-02T18:17:06.452Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d4/25292a6dfc125f6b6528fe6af31f5e996e19bf73ca8e3ce6eb7fa5b95885/ruff-0.15.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89dd695bc72ae76ff484ae54b7e8b0f6b50f49046e198355e44ea656e521fef9", size = 11988891, upload-time = "2026-04-02T18:17:18.575Z" }, + { url = "https://files.pythonhosted.org/packages/13/e1/1eebcb885c10e19f969dcb93d8413dfee8172578709d7ee933640f5e7147/ruff-0.15.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce187224ef1de1bd225bc9a152ac7102a6171107f026e81f317e4257052916d5", size = 11480576, upload-time = "2026-04-02T18:16:52.986Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/a1548ac378a78332a4c3dcf4a134c2475a36d2a22ddfa272acd574140b50/ruff-0.15.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0c7c341f68adb01c488c3b7d4b49aa8ea97409eae6462d860a79cf55f431b6", size = 11254525, upload-time = "2026-04-02T18:17:02.041Z" }, + { url = "https://files.pythonhosted.org/packages/42/aa/4bb3af8e61acd9b1281db2ab77e8b2c3c5e5599bf2a29d4a942f1c62b8d6/ruff-0.15.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:55cc15eee27dc0eebdfcb0d185a6153420efbedc15eb1d38fe5e685657b0f840", size = 11204072, upload-time = "2026-04-02T18:17:13.581Z" }, + { url = "https://files.pythonhosted.org/packages/69/48/d550dc2aa6e423ea0bcc1d0ff0699325ffe8a811e2dba156bd80750b86dc/ruff-0.15.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6537f6eed5cda688c81073d46ffdfb962a5f29ecb6f7e770b2dc920598997ed", size = 10594998, upload-time = "2026-04-02T18:16:46.369Z" }, + { url = "https://files.pythonhosted.org/packages/63/47/321167e17f5344ed5ec6b0aa2cff64efef5f9e985af8f5622cfa6536043f/ruff-0.15.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6d3fcbca7388b066139c523bda744c822258ebdcfbba7d24410c3f454cc9af71", size = 10359769, upload-time = "2026-04-02T18:17:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/67/5e/074f00b9785d1d2c6f8c22a21e023d0c2c1817838cfca4c8243200a1fa87/ruff-0.15.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:058d8e99e1bfe79d8a0def0b481c56059ee6716214f7e425d8e737e412d69677", size = 10850236, upload-time = "2026-04-02T18:16:48.749Z" }, + { url = "https://files.pythonhosted.org/packages/76/37/804c4135a2a2caf042925d30d5f68181bdbd4461fd0d7739da28305df593/ruff-0.15.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8e1ddb11dbd61d5983fa2d7d6370ef3eb210951e443cace19594c01c72abab4c", size = 11358343, upload-time = "2026-04-02T18:16:55.068Z" }, + { url = "https://files.pythonhosted.org/packages/88/3d/1364fcde8656962782aa9ea93c92d98682b1ecec2f184e625a965ad3b4a6/ruff-0.15.9-py3-none-win32.whl", hash = "sha256:bde6ff36eaf72b700f32b7196088970bf8fdb2b917b7accd8c371bfc0fd573ec", size = 10583382, upload-time = "2026-04-02T18:17:04.261Z" }, + { url = "https://files.pythonhosted.org/packages/4c/56/5c7084299bd2cacaa07ae63a91c6f4ba66edc08bf28f356b24f6b717c799/ruff-0.15.9-py3-none-win_amd64.whl", hash = "sha256:45a70921b80e1c10cf0b734ef09421f71b5aa11d27404edc89d7e8a69505e43d", size = 11744969, upload-time = "2026-04-02T18:16:59.611Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/76704c4f312257d6dbaae3c959add2a622f63fcca9d864659ce6d8d97d3d/ruff-0.15.9-py3-none-win_arm64.whl", hash = "sha256:0694e601c028fd97dc5c6ee244675bc241aeefced7ef80cd9c6935a871078f53", size = 11005870, upload-time = "2026-04-02T18:17:15.773Z" }, ] [[package]] @@ -5566,22 +5553,22 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.48" +version = "2.0.49" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737, upload-time = "2026-03-02T15:49:13.207Z" }, - { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020, upload-time = "2026-03-02T15:50:34.547Z" }, - { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983, upload-time = "2026-03-02T15:53:25.542Z" }, - { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690, upload-time = "2026-03-02T15:50:36.277Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738, upload-time = "2026-03-02T15:53:27.519Z" }, - { url = "https://files.pythonhosted.org/packages/4b/44/1d257d9f9556661e7bdc83667cc414ba210acfc110c82938cb3611eea58f/sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99", size = 2115546, upload-time = "2026-03-02T15:54:31.591Z" }, - { url = "https://files.pythonhosted.org/packages/f2/af/c3c7e1f3a2b383155a16454df62ae8c62a30dd238e42e68c24cebebbfae6/sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a", size = 2142484, upload-time = "2026-03-02T15:54:34.072Z" }, - { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, + { url = "https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b", size = 2157681, upload-time = "2026-04-03T16:53:07.132Z" }, + { url = "https://files.pythonhosted.org/packages/50/84/b2a56e2105bd11ebf9f0b93abddd748e1a78d592819099359aa98134a8bf/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982", size = 3338976, upload-time = "2026-04-03T17:07:40Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672", size = 3351937, upload-time = "2026-04-03T17:12:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2f/6fd118563572a7fe475925742eb6b3443b2250e346a0cc27d8d408e73773/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e", size = 3281646, upload-time = "2026-04-03T17:07:41.949Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d7/410f4a007c65275b9cf82354adb4bb8ba587b176d0a6ee99caa16fe638f8/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750", size = 3316695, upload-time = "2026-04-03T17:12:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/d9/95/81f594aa60ded13273a844539041ccf1e66c5a7bed0a8e27810a3b52d522/sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0", size = 2117483, upload-time = "2026-04-03T17:05:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4", size = 2144494, upload-time = "2026-04-03T17:05:42.282Z" }, + { url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" }, ] [[package]] @@ -5722,7 +5709,7 @@ wheels = [ [[package]] name = "tablestore" -version = "6.4.2" +version = "6.4.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -5735,9 +5722,9 @@ dependencies = [ { name = "six" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/07/afa1d18521bab13bb813066892b73589937fcf68aea63a54b0b14dae17b5/tablestore-6.4.2.tar.gz", hash = "sha256:5251e14b7c7ebf3d49d37dde957b49c7dba04ee8715c2650109cc02f3b89cc77", size = 5071435, upload-time = "2026-03-26T15:39:06.498Z" } +sdist = { url = "https://files.pythonhosted.org/packages/85/0b/c875c2314d472eed9f9644a94ae0aa7e702a6084779a0136e539d5e7ed32/tablestore-6.4.3.tar.gz", hash = "sha256:4981139e68705052ade6341060a4b6238b1fb9a8c18b43a77383fda14f7554a9", size = 5072450, upload-time = "2026-03-31T04:34:37.832Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/3f/5fb3e8e5de36934fe38986b4e861657cebb3a6dfd97d32224cd40fc66359/tablestore-6.4.2-py3-none-any.whl", hash = "sha256:98c4cffa5eace4a3ea6fc2425263e733093c2baa43537f25dbaaf02e2b7882d8", size = 5114987, upload-time = "2026-03-26T15:39:04.074Z" }, + { url = "https://files.pythonhosted.org/packages/39/e0/e11626aea61e1352dafe7707c548d482769afd3ca28f45653d380ba85a5d/tablestore-6.4.3-py3-none-any.whl", hash = "sha256:207b89324cd4157db4559c7619d42b9510a55c0565f00a439389f14426d114c5", size = 5115764, upload-time = "2026-03-31T04:34:35.761Z" }, ] [[package]] @@ -5763,7 +5750,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/20/81/be13f417065200182 [[package]] name = "tcvectordb" -version = "2.1.0" +version = "2.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, @@ -5776,9 +5763,9 @@ dependencies = [ { name = "ujson" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/4c/3510489c20823c045a4f84c3f656b1af00b3fbbfa36efc494cf01492521f/tcvectordb-2.1.0.tar.gz", hash = "sha256:382615573f2b6d3e21535b686feac8895169b8eb56078fc73abb020676a1622f", size = 85691, upload-time = "2026-03-25T12:55:27.509Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/10/41a7cc192720a79f40d470cabec308f8d0ed2547371294eafde0dfd8136b/tcvectordb-2.1.1.tar.gz", hash = "sha256:37d4a14f22c23f777e99069a102ceae786713117fc848c067a8e8e363252e621", size = 93896, upload-time = "2026-03-30T10:05:27.788Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/cf/7f340b4dc30ed0d2758915d1c2a4b2e9f0c90ce4f322b7cf17e571c80a45/tcvectordb-2.1.0-py3-none-any.whl", hash = "sha256:afbfc5f82bda70480921b2308148cbd0c51c8b45b3eef6cea64ddd003c7577e9", size = 99615, upload-time = "2026-03-25T12:55:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/e0/b6/2ab105d612165d274e1257b085a2cd64738220c4cbc0341887096b4d1977/tcvectordb-2.1.1-py3-none-any.whl", hash = "sha256:9a5090d3491ea087b25e5b72ffe5100f6330c05593d77f82bf8f893553dfae98", size = 107672, upload-time = "2026-03-30T10:05:25.949Z" }, ] [[package]] @@ -6016,14 +6003,14 @@ wheels = [ [[package]] name = "types-cffi" -version = "2.0.0.20260316" +version = "2.0.0.20260402" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "types-setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/4c/805b40b094eb3fd60f8d17fa7b3c58a33781311a95d0e6a74da0751ce294/types_cffi-2.0.0.20260316.tar.gz", hash = "sha256:8fb06ed4709675c999853689941133affcd2250cd6121cc11fd22c0d81ad510c", size = 17399, upload-time = "2026-03-16T07:54:43.059Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/85/3896bfcb4e7c32904f762c36ff0afa96d3e39bfce5a95a41635af79c8761/types_cffi-2.0.0.20260402.tar.gz", hash = "sha256:47e1320c009f630c59c55c8e3d2b8c501e280babf52e92f6109cbfb0864ba367", size = 17476, upload-time = "2026-04-02T04:21:09.332Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/5e/9f1a709225ad9d0e1d7a6e4366ff285f0113c749e882d6cbeb40eab32e75/types_cffi-2.0.0.20260316-py3-none-any.whl", hash = "sha256:dd504698029db4c580385f679324621cc64d886e6a23e9821d52bc5169251302", size = 20096, upload-time = "2026-03-16T07:54:41.994Z" }, + { url = "https://files.pythonhosted.org/packages/ae/26/aacfef05841e31c65f889ae4225c6bce6b84cd5d3882c42a3661030f29ee/types_cffi-2.0.0.20260402-py3-none-any.whl", hash = "sha256:f647a400fba0a31d603479169d82ee5359db79bd1136e41dc7e6489296e3a2b2", size = 20103, upload-time = "2026-04-02T04:21:08.199Z" }, ] [[package]] @@ -6037,20 +6024,20 @@ wheels = [ [[package]] name = "types-defusedxml" -version = "0.7.0.20250822" +version = "0.7.0.20260402" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/4a/5b997ae87bf301d1796f72637baa4e0e10d7db17704a8a71878a9f77f0c0/types_defusedxml-0.7.0.20250822.tar.gz", hash = "sha256:ba6c395105f800c973bba8a25e41b215483e55ec79c8ca82b6fe90ba0bc3f8b2", size = 10590, upload-time = "2025-08-22T03:02:59.547Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/3c/8e1243dda2fef73be93081d896503352fb92e2351b0b17ac172bbdb70ebf/types_defusedxml-0.7.0.20260402.tar.gz", hash = "sha256:4cc91b225e77c7fcf88b3fb7d821a37fb4e14530727c790b6b8a19f2968d6074", size = 10604, upload-time = "2026-04-02T04:19:00.265Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/73/8a36998cee9d7c9702ed64a31f0866c7f192ecffc22771d44dbcc7878f18/types_defusedxml-0.7.0.20250822-py3-none-any.whl", hash = "sha256:5ee219f8a9a79c184773599ad216123aedc62a969533ec36737ec98601f20dcf", size = 13430, upload-time = "2025-08-22T03:02:58.466Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4e/68f85712dfbcc929c54d57e9b0e7503c198fa65896cae2f6337840ab1cc5/types_defusedxml-0.7.0.20260402-py3-none-any.whl", hash = "sha256:200f3cb340c3c576adeb28cf365399e9bb059b34662b86ad4617692284c98bdb", size = 13434, upload-time = "2026-04-02T04:18:59.263Z" }, ] [[package]] name = "types-deprecated" -version = "1.3.1.20260130" +version = "1.3.1.20260402" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/97/9924e496f88412788c432891cacd041e542425fe0bffff4143a7c1c89ac4/types_deprecated-1.3.1.20260130.tar.gz", hash = "sha256:726b05e5e66d42359b1d6631835b15de62702588c8a59b877aa4b1e138453450", size = 8455, upload-time = "2026-01-30T03:58:17.401Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/ff/7e237c5118c1bd15e5205789901f7e01db232b0c61ca7c7c05de0394f5da/types_deprecated-1.3.1.20260402.tar.gz", hash = "sha256:00828ef7dce735d778583d00611f97da05b86b783ee14b0f22af2f945363cd12", size = 8481, upload-time = "2026-04-02T04:18:28.704Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/b2/6f920582af7efcd37165cd6321707f3ad5839dd24565a8a982f2bd9c6fd1/types_deprecated-1.3.1.20260130-py3-none-any.whl", hash = "sha256:593934d85c38ca321a9d301f00c42ffe13e4cf830b71b10579185ba0ce172d9a", size = 9077, upload-time = "2026-01-30T03:58:16.633Z" }, + { url = "https://files.pythonhosted.org/packages/ed/3c/59aa775db5f69eba978390c33e1fd617817381cd87424ac1cff4bf2fb6c5/types_deprecated-1.3.1.20260402-py3-none-any.whl", hash = "sha256:ddf1813bd99cd1c00358cb0cb079878fdaa74509e7e482b79627f74f768f31a9", size = 9077, upload-time = "2026-04-02T04:18:27.867Z" }, ] [[package]] @@ -6064,40 +6051,40 @@ wheels = [ [[package]] name = "types-flask-cors" -version = "6.0.0.20250809" +version = "6.0.0.20260402" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "flask" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/e0/e5dd841bf475765fb61cb04c1e70d2fd0675a0d4ddfacd50a333eafe7267/types_flask_cors-6.0.0.20250809.tar.gz", hash = "sha256:24380a2b82548634c0931d50b9aafab214eea9f85dcc04f15ab1518752a7e6aa", size = 9951, upload-time = "2025-08-09T03:16:37.454Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/59/84d8ed3801cbf28876067387e1055467e94e3dd404e93e35fe2ec5e46729/types_flask_cors-6.0.0.20260402.tar.gz", hash = "sha256:57350b504328df7ec13a12599e67939189cb644c5d0efec9af80ed03c592052c", size = 10126, upload-time = "2026-04-02T04:20:57.954Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/5e/1e60c29eb5796233d4d627ca4979c4ae8da962fd0aae0cdb6e3e6a807bbc/types_flask_cors-6.0.0.20250809-py3-none-any.whl", hash = "sha256:f6d660dddab946779f4263cb561bffe275d86cb8747ce02e9fec8d340780131b", size = 9971, upload-time = "2025-08-09T03:16:36.593Z" }, + { url = "https://files.pythonhosted.org/packages/51/71/d86f7644a18a8ccdddf50b9969fc94abbecd0ac52594880dc5667ca53e5e/types_flask_cors-6.0.0.20260402-py3-none-any.whl", hash = "sha256:e018d34946c110f5acfa71cc708ec66b47c4292131647e54889600c20892ca26", size = 9990, upload-time = "2026-04-02T04:20:57.12Z" }, ] [[package]] name = "types-flask-migrate" -version = "4.1.0.20250809" +version = "4.1.0.20260402" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "flask" }, { name = "flask-sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d5/d1/d11799471725b7db070c4f1caa3161f556230d4fb5dad76d23559da1be4d/types_flask_migrate-4.1.0.20250809.tar.gz", hash = "sha256:fdf97a262c86aca494d75874a2374e84f2d37bef6467d9540fa3b054b67db04e", size = 8636, upload-time = "2025-08-09T03:17:03.957Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/85/291317e13f72d5b2b6c1fe2c59c77a45d07bb225bf5bb2768da6a7b96351/types_flask_migrate-4.1.0.20260402.tar.gz", hash = "sha256:8e0062f063ecbe5c73b53ffc1e86f4d6de5ab970142c7d2dea939c5680ba817a", size = 8717, upload-time = "2026-04-02T04:21:45.77Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/53/f5fd40fb6c21c1f8e7da8325f3504492d027a7921d5c80061cd434c3a0fc/types_flask_migrate-4.1.0.20250809-py3-none-any.whl", hash = "sha256:92ad2c0d4000a53bf1e2f7813dd067edbbcc4c503961158a763e2b0ae297555d", size = 8648, upload-time = "2025-08-09T03:17:02.952Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d9/716b9cb9fca0f87e95f573e21e5ffe83d1cf9919ceb2e1cca8bc71488746/types_flask_migrate-4.1.0.20260402-py3-none-any.whl", hash = "sha256:6989d40d3cfae1c5f70c8f20ba39e714949b633329cc23b2dd00e82fd5b07d1c", size = 8669, upload-time = "2026-04-02T04:21:44.967Z" }, ] [[package]] name = "types-gevent" -version = "25.9.0.20260322" +version = "25.9.0.20260402" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "types-greenlet" }, { name = "types-psutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/f0/14a99ddcaa69b559fa7cec8c9de880b792bebb0b848ae865d94ea9058533/types_gevent-25.9.0.20260322.tar.gz", hash = "sha256:91257920845762f09753c08aa20fad1743ac13d2de8bcf23f4b8fe967d803732", size = 38241, upload-time = "2026-03-22T04:08:55.213Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/2f/a2056079f14aeacf538b51b0e6585328c3584fa8e6f4758214c9773ea4b0/types_gevent-25.9.0.20260402.tar.gz", hash = "sha256:24297e6f5733e187a517f08dde6df7b2147e14f7de4d343148f410dffebb5381", size = 38270, upload-time = "2026-04-02T04:22:00.125Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/0f/964440b57eb4ddb4aca03479a4093852e1ce79010d1c5967234e6f5d6bd9/types_gevent-25.9.0.20260322-py3-none-any.whl", hash = "sha256:21b3c269b3a20ecb0e4668289c63b97d21694d84a004ab059c1e32ab970eacc2", size = 55500, upload-time = "2026-03-22T04:08:54.103Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2f/995920b5cc58bc9041ded8ea2fda32719f6c513bc6e43a0c5234780936db/types_gevent-25.9.0.20260402-py3-none-any.whl", hash = "sha256:178ba12e426c987dd69ef0b8ce9f1095a965103a0d673294831f49f7127bc5ba", size = 55494, upload-time = "2026-04-02T04:21:59.144Z" }, ] [[package]] @@ -6111,14 +6098,14 @@ wheels = [ [[package]] name = "types-html5lib" -version = "1.1.11.20251117" +version = "1.1.11.20260402" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "types-webencodings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c8/f3/d9a1bbba7b42b5558a3f9fe017d967f5338cf8108d35991d9b15fdea3e0d/types_html5lib-1.1.11.20251117.tar.gz", hash = "sha256:1a6a3ac5394aa12bf547fae5d5eff91dceec46b6d07c4367d9b39a37f42f201a", size = 18100, upload-time = "2025-11-17T03:08:00.78Z" } +sdist = { url = "https://files.pythonhosted.org/packages/13/95/74eabb3bd0bb2f2b3a8ba56a55e87ee4b76f2b39e2a690eca399deffc837/types_html5lib-1.1.11.20260402.tar.gz", hash = "sha256:a167a30b9619a6eea82ec8b8948044859e033966a4721db34187d647c3a6c1f3", size = 18268, upload-time = "2026-04-02T04:21:56.528Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/ab/f5606db367c1f57f7400d3cb3bead6665ee2509621439af1b29c35ef6f9e/types_html5lib-1.1.11.20251117-py3-none-any.whl", hash = "sha256:2a3fc935de788a4d2659f4535002a421e05bea5e172b649d33232e99d4272d08", size = 24302, upload-time = "2025-11-17T03:07:59.996Z" }, + { url = "https://files.pythonhosted.org/packages/79/a9/fac9d4313b1851620610f46d086ba288482c0d5384ebf6feafb5bc4bdd15/types_html5lib-1.1.11.20260402-py3-none-any.whl", hash = "sha256:245d02cf53ef62d7342268c53dbc2af2d200849feec03f77f5909655cb54ab0d", size = 24314, upload-time = "2026-04-02T04:21:55.659Z" }, ] [[package]] @@ -6168,11 +6155,11 @@ wheels = [ [[package]] name = "types-openpyxl" -version = "3.1.5.20260322" +version = "3.1.5.20260402" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/77/bf/15240de4d68192d2a1f385ef2f6f1ecb29b85d2f3791dd2e2d5b980be30f/types_openpyxl-3.1.5.20260322.tar.gz", hash = "sha256:a61d66ebe1e49697853c6db8e0929e1cda2c96755e71fb676ed7fc48dfdcf697", size = 101325, upload-time = "2026-03-22T04:08:40.426Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/8f/d9daf094e0bb468b26e74c1bf9e0170e58c3f16e583d244e9f32078b6bcc/types_openpyxl-3.1.5.20260402.tar.gz", hash = "sha256:855ad28d47c0965048082dfca424d6ebd54d8861d72abcee9106ba5868899e7f", size = 101310, upload-time = "2026-04-02T04:17:37.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/b4/c14191b30bcb266365b124b2bb4e67ecd68425a78ba77ee026f33667daa9/types_openpyxl-3.1.5.20260322-py3-none-any.whl", hash = "sha256:2f515f0b0bbfb04bfb587de34f7522d90b5151a8da7bbbd11ecec4ca40f64238", size = 166102, upload-time = "2026-03-22T04:08:39.174Z" }, + { url = "https://files.pythonhosted.org/packages/58/ee/a0b22012076cf23b73fbb82d9c40843cbf6b1d228d7a2dc883da0a905a16/types_openpyxl-3.1.5.20260402-py3-none-any.whl", hash = "sha256:1d149989f0aad4e2074e96b87a045136399e27bc2a33cfefcd0eb4cad8ea5b4c", size = 166046, upload-time = "2026-04-02T04:17:36.162Z" }, ] [[package]] @@ -6186,20 +6173,20 @@ wheels = [ [[package]] name = "types-protobuf" -version = "6.32.1.20260221" +version = "7.34.1.20260403" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5f/e2/9aa4a3b2469508bd7b4e2ae11cbedaf419222a09a1b94daffcd5efca4023/types_protobuf-6.32.1.20260221.tar.gz", hash = "sha256:6d5fb060a616bfb076cbb61b4b3c3969f5fc8bec5810f9a2f7e648ee5cbcbf6e", size = 64408, upload-time = "2026-02-21T03:55:13.916Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b3/c2e407ea36e0e4355c135127cee1b88a2cc9a2c92eafca50a360ab9f2708/types_protobuf-7.34.1.20260403.tar.gz", hash = "sha256:8d7881867888e667eb9563c08a916fccdc12bdb5f9f34c31d217cce876e36765", size = 68782, upload-time = "2026-04-03T04:18:09.428Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/e8/1fd38926f9cf031188fbc5a96694203ea6f24b0e34bd64a225ec6f6291ba/types_protobuf-6.32.1.20260221-py3-none-any.whl", hash = "sha256:da7cdd947975964a93c30bfbcc2c6841ee646b318d3816b033adc2c4eb6448e4", size = 77956, upload-time = "2026-02-21T03:55:12.894Z" }, + { url = "https://files.pythonhosted.org/packages/7d/95/24fb0f6fe37b41cf94f9b9912712645e17d8048d4becaf37c1607ddd8e32/types_protobuf-7.34.1.20260403-py3-none-any.whl", hash = "sha256:16d9bbca52ab0f306279958878567df2520f3f5579059419b0ce149a0ad1e332", size = 86011, upload-time = "2026-04-03T04:18:08.245Z" }, ] [[package]] name = "types-psutil" -version = "7.2.2.20260130" +version = "7.2.2.20260402" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/14/fc5fb0a6ddfadf68c27e254a02ececd4d5c7fdb0efcb7e7e917a183497fb/types_psutil-7.2.2.20260130.tar.gz", hash = "sha256:15b0ab69c52841cf9ce3c383e8480c620a4d13d6a8e22b16978ebddac5590950", size = 26535, upload-time = "2026-01-30T03:58:14.116Z" } +sdist = { url = "https://files.pythonhosted.org/packages/31/a2/a608db0caf0d71bd231305dc3ab3f5d65624d77761003696a3ca8c6fad40/types_psutil-7.2.2.20260402.tar.gz", hash = "sha256:9f36eebf15ad8487f8004ed67c8e008b84b63ba00cfb709a3f60275058217329", size = 26522, upload-time = "2026-04-02T04:18:47.916Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/d7/60974b7e31545d3768d1770c5fe6e093182c3bfd819429b33133ba6b3e89/types_psutil-7.2.2.20260130-py3-none-any.whl", hash = "sha256:15523a3caa7b3ff03ac7f9b78a6470a59f88f48df1d74a39e70e06d2a99107da", size = 32876, upload-time = "2026-01-30T03:58:13.172Z" }, + { url = "https://files.pythonhosted.org/packages/81/8a/f4b3ca3154e8a77df91eb7a28c208af721d48f8a4aca667f582523a0beff/types_psutil-7.2.2.20260402-py3-none-any.whl", hash = "sha256:653d1fd908e68cc0666754b16a0cee28efbded0c401caa5314d2aeea67f227cd", size = 32860, upload-time = "2026-04-02T04:18:46.671Z" }, ] [[package]] @@ -6213,14 +6200,14 @@ wheels = [ [[package]] name = "types-pygments" -version = "2.19.0.20251121" +version = "2.20.0.20260406" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "types-docutils" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/3b/cd650700ce9e26b56bd1a6aa4af397bbbc1784e22a03971cb633cdb0b601/types_pygments-2.19.0.20251121.tar.gz", hash = "sha256:eef114fde2ef6265365522045eac0f8354978a566852f69e75c531f0553822b1", size = 18590, upload-time = "2025-11-21T03:03:46.623Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/bd/d17c28a4c65c556bc4c4bc8f363aa2fbfc91b397e3c0019839d74d9ead31/types_pygments-2.20.0.20260406.tar.gz", hash = "sha256:d3ed7ecd7c34a382459d28ce624b87e1dee03d6844e43aa7590ef4b8c7c9dfce", size = 19486, upload-time = "2026-04-06T04:33:59.632Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/8a/9244b21f1d60dcc62e261435d76b02f1853b4771663d7ec7d287e47a9ba9/types_pygments-2.19.0.20251121-py3-none-any.whl", hash = "sha256:cb3bfde34eb75b984c98fb733ce4f795213bd3378f855c32e75b49318371bb25", size = 25674, upload-time = "2025-11-21T03:03:45.72Z" }, + { url = "https://files.pythonhosted.org/packages/eb/00/dca7518e6f99ce0f235ec1c6512593ee4bd25109ae1c912bf9ee836a26e1/types_pygments-2.20.0.20260406-py3-none-any.whl", hash = "sha256:6bb0c79874c304977e1c097f7007140e16fe78c443329154db803d7910d945b3", size = 27278, upload-time = "2026-04-06T04:33:58.744Z" }, ] [[package]] @@ -6247,11 +6234,11 @@ wheels = [ [[package]] name = "types-python-dateutil" -version = "2.9.0.20260323" +version = "2.9.0.20260402" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e9/02/f72df9ef5ffc4f959b83cb80c8aa03eb8718a43e563ecd99ccffe265fa89/types_python_dateutil-2.9.0.20260323.tar.gz", hash = "sha256:a107aef5841db41ace381dbbbd7e4945220fc940f7a72172a0be5a92d9ab7164", size = 16897, upload-time = "2026-03-23T04:15:14.829Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/30/c5d9efbff5422b20c9551dc5af237d1ab0c3d33729a9b3239a876ca47dd4/types_python_dateutil-2.9.0.20260402.tar.gz", hash = "sha256:a980142b9966713acb382c467e35c5cc4208a2f91b10b8d785a0ae6765df6c0b", size = 16941, upload-time = "2026-04-02T04:18:35.834Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/c1/b661838b97453e699a215451f2e22cee750eaaf4ea4619b34bdaf01221a4/types_python_dateutil-2.9.0.20260323-py3-none-any.whl", hash = "sha256:a23a50a07f6eb87e729d4cb0c2eb511c81761eeb3f505db2c1413be94aae8335", size = 18433, upload-time = "2026-03-23T04:15:13.683Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/fe753bf8329c8c3c1addcba1d2bf716c33898216757abb24f8b80f82d040/types_python_dateutil-2.9.0.20260402-py3-none-any.whl", hash = "sha256:7827e6a9c93587cc18e766944254d1351a2396262e4abe1510cbbd7601c5e01f", size = 18436, upload-time = "2026-04-02T04:18:34.806Z" }, ] [[package]] @@ -6265,11 +6252,11 @@ wheels = [ [[package]] name = "types-pywin32" -version = "311.0.0.20260323" +version = "311.0.0.20260402" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/cc/f03ddb7412ac2fc2238358b617c2d5919ba96812dff8d3081f3b2754bb83/types_pywin32-311.0.0.20260323.tar.gz", hash = "sha256:2e8dc6a59fedccbc51b241651ce1e8aa58488934f517debf23a9c6d0ff329b4b", size = 332263, upload-time = "2026-03-23T04:15:20.004Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/f0/fc3c923b5d7822f3a93c7b242a69de0e1945e7c153cc5367074621a6509f/types_pywin32-311.0.0.20260402.tar.gz", hash = "sha256:637f041065f02fb49cbaba530ae8cf2e483b5d2c145a9bf97fd084c3e913c7e3", size = 332312, upload-time = "2026-04-02T04:18:52.748Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/82/d786d5d8b846e3cbe1ee52da8945560b111c789b42c3771b2129b312ab94/types_pywin32-311.0.0.20260323-py3-none-any.whl", hash = "sha256:2f2b03fc72ae77ccbb0ee258da0f181c3a38bd8602f6e332e42587b3b0d5f095", size = 395435, upload-time = "2026-03-23T04:15:18.76Z" }, + { url = "https://files.pythonhosted.org/packages/80/0c/a2ee20785df4ebcda6d6ec62d58b7c08a37072f9d00cda4f9548e9c8e5aa/types_pywin32-311.0.0.20260402-py3-none-any.whl", hash = "sha256:4db644fcf40ee85a3ee2551f110d009e427c01569ed4670bb53cfe999df0929f", size = 395413, upload-time = "2026-04-02T04:18:51.529Z" }, ] [[package]] @@ -6296,11 +6283,11 @@ wheels = [ [[package]] name = "types-regex" -version = "2026.3.32.20260329" +version = "2026.4.4.20260405" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/d8/a3aca5775c573e56d201bbd76a827b84d851a4bce28e189e5acb9c7a0d15/types_regex-2026.3.32.20260329.tar.gz", hash = "sha256:12653e44694cb3e3ccdc39bab3d433d2a83fec1c01220e6871fd6f3cf434675c", size = 13111, upload-time = "2026-03-29T04:27:04.759Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/9c/dd7b36fe87902a161a69c4a6959e3a6afae09c2c600916beb1aecd300870/types_regex-2026.4.4.20260405.tar.gz", hash = "sha256:993b76a255d9b83fd68eed2fc52b2746be51a93b833796be4fcf9412efa0da51", size = 13143, upload-time = "2026-04-05T04:26:56.614Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/f4/a1db307e56753c49fb15fc88d70fadeb3f38897b28cab645cddd18054c79/types_regex-2026.3.32.20260329-py3-none-any.whl", hash = "sha256:861d0893bcfe08a57eb7486a502014e29dc2721d46dd5130798fbccafdb31cc0", size = 11128, upload-time = "2026-03-29T04:27:03.854Z" }, + { url = "https://files.pythonhosted.org/packages/51/83/5dbae203616699890efcdb2a2670d62baf5ed93634f75d793157f1edefb3/types_regex-2026.4.4.20260405-py3-none-any.whl", hash = "sha256:40443cb88c43b9940dd4c904e251be7e65dab3798b2cf6f5ff19501ae99b2ab5", size = 11119, upload-time = "2026-04-05T04:26:55.636Z" }, ] [[package]] @@ -6326,32 +6313,32 @@ wheels = [ [[package]] name = "types-setuptools" -version = "82.0.0.20260210" +version = "82.0.0.20260402" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4b/90/796ac8c774a7f535084aacbaa6b7053d16fff5c630eff87c3ecff7896c37/types_setuptools-82.0.0.20260210.tar.gz", hash = "sha256:d9719fbbeb185254480ade1f25327c4654f8c00efda3fec36823379cebcdee58", size = 44768, upload-time = "2026-02-10T04:22:02.107Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/f8/74f8a76b4311e70772c0df8f2d432040a3b0facd7bcce6b72b0b26e1746b/types_setuptools-82.0.0.20260402.tar.gz", hash = "sha256:63d2b10ba7958396ad79bbc24d2f6311484e452daad4637ffd40407983a27069", size = 44805, upload-time = "2026-04-02T04:17:49.229Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/54/3489432b1d9bc713c9d8aa810296b8f5b0088403662959fb63a8acdbd4fc/types_setuptools-82.0.0.20260210-py3-none-any.whl", hash = "sha256:5124a7daf67f195c6054e0f00f1d97c69caad12fdcf9113eba33eff0bce8cd2b", size = 68433, upload-time = "2026-02-10T04:22:00.876Z" }, + { url = "https://files.pythonhosted.org/packages/0e/e9/22451997f70ac2c5f18dc5f988750c986011fb049d9021767277119e63fa/types_setuptools-82.0.0.20260402-py3-none-any.whl", hash = "sha256:4b9a9f6c3c4c65107a3956ad6a6acbccec38e398ff6d5f78d5df7f103dadb8d6", size = 68429, upload-time = "2026-04-02T04:17:48.11Z" }, ] [[package]] name = "types-shapely" -version = "2.1.0.20250917" +version = "2.1.0.20260402" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fa/19/7f28b10994433d43b9caa66f3b9bd6a0a9192b7ce8b5a7fc41534e54b821/types_shapely-2.1.0.20250917.tar.gz", hash = "sha256:5c56670742105aebe40c16414390d35fcaa55d6f774d328c1a18273ab0e2134a", size = 26363, upload-time = "2025-09-17T02:47:44.604Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/f7/46e95b09434105d7b772d05657495f2900bae8e108fdf4e6d8b5902aa28c/types_shapely-2.1.0.20260402.tar.gz", hash = "sha256:0eb592328170433b4724430a64c309bf07ba69d5d11489d3dba21382d78f5297", size = 26481, upload-time = "2026-04-02T04:20:03.104Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/a9/554ac40810e530263b6163b30a2b623bc16aae3fb64416f5d2b3657d0729/types_shapely-2.1.0.20250917-py3-none-any.whl", hash = "sha256:9334a79339504d39b040426be4938d422cec419168414dc74972aa746a8bf3a1", size = 37813, upload-time = "2025-09-17T02:47:43.788Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/1aa3a62f5b85d4a9e649e7b42842a9e5503fef7eb50c480137a6b94f8bb1/types_shapely-2.1.0.20260402-py3-none-any.whl", hash = "sha256:8d70a16f615a104fd8abdd73e684d4e83b9dedf31d6432ecf86945b5ef0e35de", size = 37817, upload-time = "2026-04-02T04:20:02.17Z" }, ] [[package]] name = "types-simplejson" -version = "3.20.0.20250822" +version = "3.20.0.20260402" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/6b/96d43a90cd202bd552cdd871858a11c138fe5ef11aeb4ed8e8dc51389257/types_simplejson-3.20.0.20250822.tar.gz", hash = "sha256:2b0bfd57a6beed3b932fd2c3c7f8e2f48a7df3978c9bba43023a32b3741a95b0", size = 10608, upload-time = "2025-08-22T03:03:35.36Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/93/2ff2f4b8ccd942ee3a4b62c013d2c1779e416d303950060ed8b3f1a4fc11/types_simplejson-3.20.0.20260402.tar.gz", hash = "sha256:ee2bbf65830fe93270a1c0406f3474c952fe1232532c7b6f3eb9500edb308c5a", size = 10650, upload-time = "2026-04-02T04:19:26.266Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/9f/8e2c9e6aee9a2ff34f2ffce6ccd9c26edeef6dfd366fde611dc2e2c00ab9/types_simplejson-3.20.0.20250822-py3-none-any.whl", hash = "sha256:b5e63ae220ac7a1b0bb9af43b9cb8652237c947981b2708b0c776d3b5d8fa169", size = 10417, upload-time = "2025-08-22T03:03:34.485Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2a/7ba2bede9c2b25fb338d0bda9925a23b73a5ac99fd97304ebe067c090e33/types_simplejson-3.20.0.20260402-py3-none-any.whl", hash = "sha256:b3bdef21bc24fee26b80385ffea5163b6b10381089aa619fe2f8f8d3790e6148", size = 10419, upload-time = "2026-04-02T04:19:25.464Z" }, ] [[package]] @@ -6365,28 +6352,28 @@ wheels = [ [[package]] name = "types-tensorflow" -version = "2.18.0.20260322" +version = "2.18.0.20260402" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "types-protobuf" }, { name = "types-requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4a/cb/81dfaa2680031a6e087bcdfaf1c0556371098e229aee541e21c81a381065/types_tensorflow-2.18.0.20260322.tar.gz", hash = "sha256:135dc6ca06cc647a002e1bca5c5c99516fde51efd08e46c48a9b1916fc5df07f", size = 259030, upload-time = "2026-03-22T04:09:14.069Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d9/1ca68336ce7ad8c4a19001fce85f47ffae9d7ac335e5ddd73497b6bfbca4/types_tensorflow-2.18.0.20260402.tar.gz", hash = "sha256:607c4a5895d44c88c7c465410093ee050aa760c3cedab5b9662f475c5e2137d3", size = 259058, upload-time = "2026-04-02T04:22:39.113Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/0c/a178061450b640e53577e2c423ad22bf5d3f692f6bfeeb12156d02b531ef/types_tensorflow-2.18.0.20260322-py3-none-any.whl", hash = "sha256:d8776b6daacdb279e64f105f9dcbc0b8e3544b9a2f2eb71ec6ea5955081f65e6", size = 329771, upload-time = "2026-03-22T04:09:12.844Z" }, + { url = "https://files.pythonhosted.org/packages/c1/6c/0ad58c7246a5369ceb2ae16c146ac0684a0827f499a8141fc3d13743c38b/types_tensorflow-2.18.0.20260402-py3-none-any.whl", hash = "sha256:0d4a74921c457ade8f46eb09cf728a1732156678e497ce15a88b9c0c16dc2fe5", size = 329776, upload-time = "2026-04-02T04:22:37.903Z" }, ] [[package]] name = "types-tqdm" -version = "4.67.3.20260303" +version = "4.67.3.20260402" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "types-requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/64/3e7cb0f40c4bf9578098b6873df33a96f7e0de90f3a039e614d22bfde40a/types_tqdm-4.67.3.20260303.tar.gz", hash = "sha256:7bfddb506a75aedb4030fabf4f05c5638c9a3bbdf900d54ec6c82be9034bfb96", size = 18117, upload-time = "2026-03-03T04:03:49.679Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/42/e9e6688891d8db77b5795ec02b329524170892ff81bec63c4c4ca7425b30/types_tqdm-4.67.3.20260402.tar.gz", hash = "sha256:e0739f3bc5d1c801999a202f0537280aa1bc2e669c49f5be91bfb99376690624", size = 18077, upload-time = "2026-04-02T04:22:23.049Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/32/e4a1fce59155c74082f1a42d0ffafa59652bfb8cff35b04d56333877748e/types_tqdm-4.67.3.20260303-py3-none-any.whl", hash = "sha256:459decf677e4b05cef36f9012ef8d6e20578edefb6b78c15bd0b546247eda62d", size = 24572, upload-time = "2026-03-03T04:03:48.913Z" }, + { url = "https://files.pythonhosted.org/packages/4f/73/a6cf75de5be376d7b57ce6c934ae9bc90aa5be6ada4ac50a99ecbdf9763e/types_tqdm-4.67.3.20260402-py3-none-any.whl", hash = "sha256:b5d1a65fe3286e1a855e51ddebf63d3641daf9bad285afd1ec56808eb59df76e", size = 24562, upload-time = "2026-04-02T04:22:22.114Z" }, ] [[package]] diff --git a/docker/.env.example b/docker/.env.example index b2d6244b46..f20d57c71a 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -1358,6 +1358,18 @@ SSRF_POOL_KEEPALIVE_EXPIRY=5.0 # ------------------------------ COMPOSE_PROFILES=${VECTOR_STORE:-weaviate},${DB_TYPE:-postgresql} +# ------------------------------ +# Worker health check configuration for worker and worker_beat services. +# Set to false to enable the health check. +# Note: enabling the health check may cause periodic CPU spikes and increased load, +# as it establishes a broker connection and sends a Celery ping on every check interval. +# ------------------------------ +COMPOSE_WORKER_HEALTHCHECK_DISABLED=true +# Interval between health checks (e.g. 30s, 1m) +COMPOSE_WORKER_HEALTHCHECK_INTERVAL=30s +# Timeout for each health check (e.g. 30s, 1m) +COMPOSE_WORKER_HEALTHCHECK_TIMEOUT=30s + # ------------------------------ # Docker Compose Service Expose Host Port Configurations # ------------------------------ diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml index 57584cb829..5234202a62 100644 --- a/docker/docker-compose-template.yaml +++ b/docker/docker-compose-template.yaml @@ -102,11 +102,12 @@ services: # Mount the storage directory to the container, for storing user files. - ./volumes/app/storage:/app/api/storage healthcheck: - test: ["CMD-SHELL", "celery -A celery_entrypoint.celery inspect ping"] - interval: 30s - timeout: 10s + test: ["CMD-SHELL", "celery -A celery_healthcheck.celery inspect ping"] + interval: ${COMPOSE_WORKER_HEALTHCHECK_INTERVAL:-30s} + timeout: ${COMPOSE_WORKER_HEALTHCHECK_TIMEOUT:-30s} retries: 3 start_period: 60s + disable: ${COMPOSE_WORKER_HEALTHCHECK_DISABLED:-true} networks: - ssrf_proxy_network - default @@ -139,11 +140,12 @@ services: redis: condition: service_started healthcheck: - test: ["CMD-SHELL", "celery -A app.celery inspect ping"] - interval: 30s - timeout: 10s + test: ["CMD-SHELL", "celery -A celery_healthcheck.celery inspect ping"] + interval: ${COMPOSE_WORKER_HEALTHCHECK_INTERVAL:-30s} + timeout: ${COMPOSE_WORKER_HEALTHCHECK_TIMEOUT:-30s} retries: 3 start_period: 60s + disable: ${COMPOSE_WORKER_HEALTHCHECK_DISABLED:-true} networks: - ssrf_proxy_network - default diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 097fadc959..d03835e2b0 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -811,11 +811,12 @@ services: # Mount the storage directory to the container, for storing user files. - ./volumes/app/storage:/app/api/storage healthcheck: - test: ["CMD-SHELL", "celery -A celery_entrypoint.celery inspect ping"] - interval: 30s - timeout: 10s + test: ["CMD-SHELL", "celery -A celery_healthcheck.celery inspect ping"] + interval: ${COMPOSE_WORKER_HEALTHCHECK_INTERVAL:-30s} + timeout: ${COMPOSE_WORKER_HEALTHCHECK_TIMEOUT:-30s} retries: 3 start_period: 60s + disable: ${COMPOSE_WORKER_HEALTHCHECK_DISABLED:-true} networks: - ssrf_proxy_network - default @@ -848,11 +849,12 @@ services: redis: condition: service_started healthcheck: - test: ["CMD-SHELL", "celery -A app.celery inspect ping"] - interval: 30s - timeout: 10s + test: ["CMD-SHELL", "celery -A celery_healthcheck.celery inspect ping"] + interval: ${COMPOSE_WORKER_HEALTHCHECK_INTERVAL:-30s} + timeout: ${COMPOSE_WORKER_HEALTHCHECK_TIMEOUT:-30s} retries: 3 start_period: 60s + disable: ${COMPOSE_WORKER_HEALTHCHECK_DISABLED:-true} networks: - ssrf_proxy_network - default diff --git a/docker/generate_docker_compose b/docker/generate_docker_compose index bf6c1423c9..46d948f3c1 100755 --- a/docker/generate_docker_compose +++ b/docker/generate_docker_compose @@ -3,6 +3,20 @@ import os import re import sys +# Variables that exist only for Docker Compose orchestration and must NOT be +# injected into containers as environment variables. +SHARED_ENV_EXCLUDE = frozenset( + [ + # Docker Compose profile selection + "COMPOSE_PROFILES", + # Worker health check orchestration flags (consumed by docker-compose, + # not by the application running inside the container) + "COMPOSE_WORKER_HEALTHCHECK_DISABLED", + "COMPOSE_WORKER_HEALTHCHECK_INTERVAL", + "COMPOSE_WORKER_HEALTHCHECK_TIMEOUT", + ] +) + def parse_env_example(file_path): """ @@ -37,7 +51,7 @@ def generate_shared_env_block(env_vars, anchor_name="shared-api-worker-env"): """ lines = [f"x-shared-env: &{anchor_name}"] for key, default in env_vars.items(): - if key == "COMPOSE_PROFILES": + if key in SHARED_ENV_EXCLUDE: continue # If default value is empty, use ${KEY:-} if default == "": @@ -54,6 +68,7 @@ def insert_shared_env(template_path, output_path, shared_env_block, header_comme """ Inserts the shared environment variables block and header comments into the template file, removing any existing x-shared-env anchors, and generates the final docker-compose.yaml file. + Always writes with LF line endings. """ with open(template_path, "r", encoding="utf-8") as f: template_content = f.read() @@ -69,7 +84,7 @@ def insert_shared_env(template_path, output_path, shared_env_block, header_comme # Prepare the final content with header comments and shared env block final_content = f"{header_comments}\n{shared_env_block}\n\n{template_content}" - with open(output_path, "w", encoding="utf-8") as f: + with open(output_path, "w", encoding="utf-8", newline="\n") as f: f.write(final_content) print(f"Generated {output_path}") diff --git a/package.json b/package.json index 48c3acef02..ce3180214b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,6 @@ "prepare": "vp config" }, "devDependencies": { - "taze": "catalog:", "vite-plus": "catalog:" }, "engines": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a44b621b1..98e6e21bc2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,14 +7,14 @@ settings: catalogs: default: '@amplitude/analytics-browser': - specifier: 2.38.0 - version: 2.38.0 + specifier: 2.38.1 + version: 2.38.1 '@amplitude/plugin-session-replay-browser': - specifier: 1.27.5 - version: 1.27.5 + specifier: 1.27.6 + version: 1.27.6 '@antfu/eslint-config': - specifier: 7.7.3 - version: 7.7.3 + specifier: 8.0.0 + version: 8.0.0 '@base-ui/react': specifier: 1.3.0 version: 1.3.0 @@ -43,14 +43,14 @@ catalogs: specifier: 0.8.2 version: 0.8.2 '@headlessui/react': - specifier: 2.2.9 - version: 2.2.9 + specifier: 2.2.10 + version: 2.2.10 '@heroicons/react': specifier: 2.2.0 version: 2.2.0 '@hono/node-server': - specifier: 1.19.11 - version: 1.19.11 + specifier: 1.19.13 + version: 1.19.13 '@iconify-json/heroicons': specifier: 1.2.3 version: 1.2.3 @@ -88,11 +88,11 @@ catalogs: specifier: 4.7.0 version: 4.7.0 '@next/eslint-plugin-next': - specifier: 16.2.1 - version: 16.2.1 + specifier: 16.2.2 + version: 16.2.2 '@next/mdx': - specifier: 16.2.1 - version: 16.2.1 + specifier: 16.2.2 + version: 16.2.2 '@orpc/client': specifier: 1.13.13 version: 1.13.13 @@ -106,8 +106,8 @@ catalogs: specifier: 1.13.13 version: 1.13.13 '@playwright/test': - specifier: 1.58.2 - version: 1.58.2 + specifier: 1.59.1 + version: 1.59.1 '@remixicon/react': specifier: 4.9.0 version: 4.9.0 @@ -115,26 +115,26 @@ catalogs: specifier: 4.2.0 version: 4.2.0 '@sentry/react': - specifier: 10.46.0 - version: 10.46.0 + specifier: 10.47.0 + version: 10.47.0 '@storybook/addon-docs': - specifier: 10.3.3 - version: 10.3.3 + specifier: 10.3.5 + version: 10.3.5 '@storybook/addon-links': - specifier: 10.3.3 - version: 10.3.3 + specifier: 10.3.5 + version: 10.3.5 '@storybook/addon-onboarding': - specifier: 10.3.3 - version: 10.3.3 + specifier: 10.3.5 + version: 10.3.5 '@storybook/addon-themes': - specifier: 10.3.3 - version: 10.3.3 + specifier: 10.3.5 + version: 10.3.5 '@storybook/nextjs-vite': - specifier: 10.3.3 - version: 10.3.3 + specifier: 10.3.5 + version: 10.3.5 '@storybook/react': - specifier: 10.3.3 - version: 10.3.3 + specifier: 10.3.5 + version: 10.3.5 '@streamdown/math': specifier: 1.0.2 version: 1.0.2 @@ -154,23 +154,26 @@ catalogs: specifier: 4.2.2 version: 4.2.2 '@tanstack/eslint-plugin-query': - specifier: 5.95.2 - version: 5.95.2 + specifier: 5.96.2 + version: 5.96.2 '@tanstack/react-devtools': - specifier: 0.10.0 - version: 0.10.0 + specifier: 0.10.2 + version: 0.10.2 '@tanstack/react-form': - specifier: 1.28.5 - version: 1.28.5 + specifier: 1.28.6 + version: 1.28.6 '@tanstack/react-form-devtools': - specifier: 0.2.19 - version: 0.2.19 + specifier: 0.2.20 + version: 0.2.20 '@tanstack/react-query': - specifier: 5.95.2 - version: 5.95.2 + specifier: 5.96.2 + version: 5.96.2 '@tanstack/react-query-devtools': - specifier: 5.95.2 - version: 5.95.2 + specifier: 5.96.2 + version: 5.96.2 + '@tanstack/react-virtual': + specifier: 3.13.23 + version: 3.13.23 '@testing-library/dom': specifier: 10.4.1 version: 10.4.1 @@ -202,8 +205,8 @@ catalogs: specifier: 0.6.4 version: 0.6.4 '@types/node': - specifier: 25.5.0 - version: 25.5.0 + specifier: 25.5.2 + version: 25.5.2 '@types/qs': specifier: 6.15.0 version: 6.15.0 @@ -213,33 +216,27 @@ catalogs: '@types/react-dom': specifier: 19.2.3 version: 19.2.3 - '@types/react-syntax-highlighter': - specifier: 15.5.13 - version: 15.5.13 - '@types/react-window': - specifier: 1.8.8 - version: 1.8.8 '@types/sortablejs': specifier: 1.15.9 version: 1.15.9 '@typescript-eslint/eslint-plugin': - specifier: 8.57.2 - version: 8.57.2 + specifier: 8.58.1 + version: 8.58.1 '@typescript-eslint/parser': - specifier: 8.57.2 - version: 8.57.2 + specifier: 8.58.1 + version: 8.58.1 '@typescript/native-preview': - specifier: 7.0.0-dev.20260329.1 - version: 7.0.0-dev.20260329.1 + specifier: 7.0.0-dev.20260407.1 + version: 7.0.0-dev.20260407.1 '@vitejs/plugin-react': specifier: 6.0.1 version: 6.0.1 '@vitejs/plugin-rsc': - specifier: 0.5.21 - version: 0.5.21 + specifier: 0.5.22 + version: 0.5.22 '@vitest/coverage-v8': - specifier: 4.1.1 - version: 4.1.1 + specifier: 4.1.3 + version: 4.1.3 abcjs: specifier: 6.6.2 version: 6.6.2 @@ -259,8 +256,8 @@ catalogs: specifier: 1.1.1 version: 1.1.1 code-inspector-plugin: - specifier: 1.4.5 - version: 1.4.5 + specifier: 1.5.1 + version: 1.5.1 copy-to-clipboard: specifier: 3.3.3 version: 3.3.3 @@ -298,8 +295,8 @@ catalogs: specifier: 1.45.1 version: 1.45.1 eslint: - specifier: 10.1.0 - version: 10.1.0 + specifier: 10.2.0 + version: 10.2.0 eslint-markdown: specifier: 0.6.0 version: 0.6.0 @@ -310,14 +307,11 @@ catalogs: specifier: 0.14.1 version: 0.14.1 eslint-plugin-markdown-preferences: - specifier: 0.40.3 - version: 0.40.3 + specifier: 0.41.0 + version: 0.41.0 eslint-plugin-no-barrel-files: specifier: 1.2.2 version: 1.2.2 - eslint-plugin-react-hooks: - specifier: 7.0.1 - version: 7.0.1 eslint-plugin-react-refresh: specifier: 0.5.2 version: 0.5.2 @@ -325,8 +319,8 @@ catalogs: specifier: 4.0.2 version: 4.0.2 eslint-plugin-storybook: - specifier: 10.3.3 - version: 10.3.3 + specifier: 10.3.5 + version: 10.3.5 fast-deep-equal: specifier: 3.1.3 version: 3.1.3 @@ -336,9 +330,12 @@ catalogs: happy-dom: specifier: 20.8.9 version: 20.8.9 + hast-util-to-jsx-runtime: + specifier: 2.3.6 + version: 2.3.6 hono: - specifier: 4.12.9 - version: 4.12.9 + specifier: 4.12.12 + version: 4.12.12 html-entities: specifier: 2.6.0 version: 2.6.0 @@ -346,8 +343,8 @@ catalogs: specifier: 1.11.13 version: 1.11.13 i18next: - specifier: 25.10.10 - version: 25.10.10 + specifier: 26.0.3 + version: 26.0.3 i18next-resources-to-backend: specifier: 1.2.1 version: 1.2.1 @@ -358,8 +355,8 @@ catalogs: specifier: 11.1.4 version: 11.1.4 jotai: - specifier: 2.19.0 - version: 2.19.0 + specifier: 2.19.1 + version: 2.19.1 js-audio-recorder: specifier: 1.0.7 version: 1.0.7 @@ -373,14 +370,14 @@ catalogs: specifier: 1.5.0 version: 1.5.0 katex: - specifier: 0.16.44 - version: 0.16.44 + specifier: 0.16.45 + version: 0.16.45 knip: - specifier: 6.1.0 - version: 6.1.0 + specifier: 6.3.0 + version: 6.3.0 ky: - specifier: 1.14.3 - version: 1.14.3 + specifier: 2.0.0 + version: 2.0.0 lamejs: specifier: 1.2.1 version: 1.2.1 @@ -388,8 +385,8 @@ catalogs: specifier: 0.42.0 version: 0.42.0 mermaid: - specifier: 11.13.0 - version: 11.13.0 + specifier: 11.14.0 + version: 11.14.0 mime: specifier: 4.1.0 version: 4.1.0 @@ -400,8 +397,8 @@ catalogs: specifier: 1.0.0 version: 1.0.0 next: - specifier: 16.2.1 - version: 16.2.1 + specifier: 16.2.2 + version: 16.2.2 next-themes: specifier: 0.4.6 version: 0.4.6 @@ -412,8 +409,8 @@ catalogs: specifier: 3.28.0 version: 3.28.0 postcss: - specifier: 8.5.8 - version: 8.5.8 + specifier: 8.5.9 + version: 8.5.9 qrcode.react: specifier: 4.2.0 version: 4.2.0 @@ -436,8 +433,8 @@ catalogs: specifier: 5.2.4 version: 5.2.4 react-i18next: - specifier: 16.6.6 - version: 16.6.6 + specifier: 17.0.2 + version: 17.0.2 react-multi-email: specifier: 1.0.25 version: 1.0.25 @@ -453,15 +450,9 @@ catalogs: react-sortablejs: specifier: 6.1.4 version: 6.1.4 - react-syntax-highlighter: - specifier: 15.6.6 - version: 15.6.6 react-textarea-autosize: specifier: 8.5.9 version: 8.5.9 - react-window: - specifier: 1.8.11 - version: 1.8.11 reactflow: specifier: 11.11.4 version: 11.11.4 @@ -471,15 +462,15 @@ catalogs: remark-directive: specifier: 4.0.0 version: 4.0.0 - sass: - specifier: 1.98.0 - version: 1.98.0 scheduler: specifier: 0.27.0 version: 0.27.0 sharp: specifier: 0.34.5 version: 0.34.5 + shiki: + specifier: 4.0.2 + version: 4.0.2 sortablejs: specifier: 1.15.7 version: 1.15.7 @@ -487,8 +478,8 @@ catalogs: specifier: 1.0.8 version: 1.0.8 storybook: - specifier: 10.3.3 - version: 10.3.3 + specifier: 10.3.5 + version: 10.3.5 streamdown: specifier: 2.5.0 version: 2.5.0 @@ -501,21 +492,15 @@ catalogs: tailwindcss: specifier: 4.2.2 version: 4.2.2 - taze: - specifier: 19.10.0 - version: 19.10.0 tldts: - specifier: 7.0.27 - version: 7.0.27 - tsup: - specifier: ^8.5.1 - version: 8.5.1 + specifier: 7.0.28 + version: 7.0.28 tsx: specifier: 4.21.0 version: 4.21.0 typescript: - specifier: 5.9.3 - version: 5.9.3 + specifier: 6.0.2 + version: 6.0.2 uglify-js: specifier: 3.19.3 version: 3.19.3 @@ -529,14 +514,14 @@ catalogs: specifier: 13.0.0 version: 13.0.0 vinext: - specifier: 0.0.38 - version: 0.0.38 + specifier: 0.0.40 + version: 0.0.40 vite-plugin-inspect: specifier: 12.0.0-beta.1 version: 12.0.0-beta.1 vite-plus: - specifier: 0.1.14 - version: 0.1.14 + specifier: 0.1.16 + version: 0.1.16 vitest-canvas-mock: specifier: 1.1.4 version: 1.1.4 @@ -553,58 +538,29 @@ catalogs: overrides: '@lexical/code': npm:lexical-code-no-prism@0.41.0 '@monaco-editor/loader': 1.7.0 - '@nolyfill/safe-buffer': npm:safe-buffer@^5.2.1 - array-includes: npm:@nolyfill/array-includes@^1.0.44 - array.prototype.findlast: npm:@nolyfill/array.prototype.findlast@^1.0.44 - array.prototype.findlastindex: npm:@nolyfill/array.prototype.findlastindex@^1.0.44 - array.prototype.flat: npm:@nolyfill/array.prototype.flat@^1.0.44 - array.prototype.flatmap: npm:@nolyfill/array.prototype.flatmap@^1.0.44 - array.prototype.tosorted: npm:@nolyfill/array.prototype.tosorted@^1.0.44 - assert: npm:@nolyfill/assert@^1.0.26 - brace-expansion@<2.0.2: 2.0.2 + brace-expansion@>=2.0.0 <2.0.3: 2.0.3 canvas: ^3.2.2 - devalue@<5.3.2: 5.3.2 dompurify@>=3.1.3 <=3.3.1: 3.3.2 - es-iterator-helpers: npm:@nolyfill/es-iterator-helpers@^1.0.21 esbuild@<0.27.2: 0.27.2 flatted@<=3.4.1: 3.4.2 glob@>=10.2.0 <10.5.0: 11.1.0 - hasown: npm:@nolyfill/hasown@^1.0.44 - is-arguments: npm:@nolyfill/is-arguments@^1.0.44 is-core-module: npm:@nolyfill/is-core-module@^1.0.39 - is-generator-function: npm:@nolyfill/is-generator-function@^1.0.44 - is-typed-array: npm:@nolyfill/is-typed-array@^1.0.44 - isarray: npm:@nolyfill/isarray@^1.0.44 - object.assign: npm:@nolyfill/object.assign@^1.0.44 - object.entries: npm:@nolyfill/object.entries@^1.0.44 - object.fromentries: npm:@nolyfill/object.fromentries@^1.0.44 - object.groupby: npm:@nolyfill/object.groupby@^1.0.44 - object.values: npm:@nolyfill/object.values@^1.0.44 - pbkdf2: ~3.1.5 - pbkdf2@<3.1.3: 3.1.3 + lodash@>=4.0.0 <= 4.17.23: 4.18.0 + lodash-es@>=4.0.0 <= 4.17.23: 4.18.0 picomatch@<2.3.2: 2.3.2 picomatch@>=4.0.0 <4.0.4: 4.0.4 - prismjs: ~1.30 - prismjs@<1.30.0: 1.30.0 rollup@>=4.0.0 <4.59.0: 4.59.0 safe-buffer: ^5.2.1 - safe-regex-test: npm:@nolyfill/safe-regex-test@^1.0.44 safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44 side-channel: npm:@nolyfill/side-channel@^1.0.44 smol-toml@<1.6.1: 1.6.1 solid-js: 1.9.11 string-width: ~8.2.0 - string.prototype.includes: npm:@nolyfill/string.prototype.includes@^1.0.44 - string.prototype.matchall: npm:@nolyfill/string.prototype.matchall@^1.0.44 - string.prototype.repeat: npm:@nolyfill/string.prototype.repeat@^1.0.44 - string.prototype.trimend: npm:@nolyfill/string.prototype.trimend@^1.0.44 svgo@>=3.0.0 <3.3.3: 3.3.3 tar@<=7.5.10: 7.5.11 - typed-array-buffer: npm:@nolyfill/typed-array-buffer@^1.0.44 undici@>=7.0.0 <7.24.0: 7.24.0 - vite: npm:@voidzero-dev/vite-plus-core@0.1.14 - vitest: npm:@voidzero-dev/vite-plus-test@0.1.14 - which-typed-array: npm:@nolyfill/which-typed-array@^1.0.44 + vite: npm:@voidzero-dev/vite-plus-core@0.1.16 + vitest: npm:@voidzero-dev/vite-plus-test@0.1.16 yaml@>=2.0.0 <2.8.3: 2.8.3 yauzl@<3.2.1: 3.2.1 @@ -612,12 +568,9 @@ importers: .: devDependencies: - taze: - specifier: 'catalog:' - version: 19.10.0 vite-plus: specifier: 'catalog:' - version: 0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) + version: 0.1.16(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) e2e: devDependencies: @@ -626,19 +579,19 @@ importers: version: 12.7.0 '@playwright/test': specifier: 'catalog:' - version: 1.58.2 + version: 1.59.1 '@types/node': specifier: 'catalog:' - version: 25.5.0 + version: 25.5.2 tsx: specifier: 'catalog:' version: 4.21.0 typescript: specifier: 'catalog:' - version: 5.9.3 + version: 6.0.2 vite-plus: specifier: 'catalog:' - version: 0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) + version: 0.1.16(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) packages/iconify-collections: devDependencies: @@ -650,40 +603,40 @@ importers: devDependencies: '@eslint/js': specifier: 'catalog:' - version: 10.0.1(eslint@10.1.0(jiti@2.6.1)) + version: 10.0.1(eslint@10.2.0(jiti@2.6.1)) '@types/node': specifier: 'catalog:' - version: 25.5.0 + version: 25.5.2 '@typescript-eslint/eslint-plugin': specifier: 'catalog:' - version: 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + version: 8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) '@typescript-eslint/parser': specifier: 'catalog:' - version: 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + version: 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.1(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)) + version: 4.1.3(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)) eslint: specifier: 'catalog:' - version: 10.1.0(jiti@2.6.1) - tsup: - specifier: 'catalog:' - version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + version: 10.2.0(jiti@2.6.1) typescript: specifier: 'catalog:' - version: 5.9.3 + version: 6.0.2 + vite-plus: + specifier: 'catalog:' + version: 0.1.16(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) vitest: - specifier: npm:@voidzero-dev/vite-plus-test@0.1.14 - version: '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)' + specifier: npm:@voidzero-dev/vite-plus-test@0.1.16 + version: '@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)' web: dependencies: '@amplitude/analytics-browser': specifier: 'catalog:' - version: 2.38.0 + version: 2.38.1 '@amplitude/plugin-session-replay-browser': specifier: 'catalog:' - version: 1.27.5(@amplitude/rrweb@2.0.0-alpha.37)(rollup@4.59.0) + version: 1.27.6(@amplitude/rrweb@2.0.0-alpha.37)(rollup@4.59.0) '@base-ui/react': specifier: 'catalog:' version: 1.3.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -698,7 +651,7 @@ importers: version: 0.8.2 '@headlessui/react': specifier: 'catalog:' - version: 2.2.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 2.2.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroicons/react': specifier: 'catalog:' version: 2.2.0(react@19.2.4) @@ -737,13 +690,13 @@ importers: version: 1.13.13 '@orpc/tanstack-query': specifier: 'catalog:' - version: 1.13.13(@orpc/client@1.13.13)(@tanstack/query-core@5.95.2) + version: 1.13.13(@orpc/client@1.13.13)(@tanstack/query-core@5.96.2) '@remixicon/react': specifier: 'catalog:' version: 4.9.0(react@19.2.4) '@sentry/react': specifier: 'catalog:' - version: 10.46.0(react@19.2.4) + version: 10.47.0(react@19.2.4) '@streamdown/math': specifier: 'catalog:' version: 1.0.2(react@19.2.4) @@ -752,16 +705,19 @@ importers: version: 3.2.5 '@t3-oss/env-nextjs': specifier: 'catalog:' - version: 0.13.11(typescript@5.9.3)(valibot@1.3.1(typescript@5.9.3))(zod@4.3.6) + version: 0.13.11(typescript@6.0.2)(valibot@1.3.1(typescript@6.0.2))(zod@4.3.6) '@tailwindcss/typography': specifier: 'catalog:' version: 0.5.19(tailwindcss@4.2.2) '@tanstack/react-form': specifier: 'catalog:' - version: 1.28.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 1.28.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-query': specifier: 'catalog:' - version: 5.95.2(react@19.2.4) + version: 5.96.2(react@19.2.4) + '@tanstack/react-virtual': + specifier: 'catalog:' + version: 3.13.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4) abcjs: specifier: 'catalog:' version: 6.6.2 @@ -819,6 +775,9 @@ importers: foxact: specifier: 'catalog:' version: 0.3.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + hast-util-to-jsx-runtime: + specifier: 'catalog:' + version: 2.3.6 html-entities: specifier: 'catalog:' version: 2.6.0 @@ -827,7 +786,7 @@ importers: version: 1.11.13 i18next: specifier: 'catalog:' - version: 25.10.10(typescript@5.9.3) + version: 26.0.3(typescript@6.0.2) i18next-resources-to-backend: specifier: 'catalog:' version: 1.2.1 @@ -836,7 +795,7 @@ importers: version: 11.1.4 jotai: specifier: 'catalog:' - version: 2.19.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4) + version: 2.19.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4) js-audio-recorder: specifier: 'catalog:' version: 1.0.7 @@ -851,10 +810,10 @@ importers: version: 1.5.0 katex: specifier: 'catalog:' - version: 0.16.44 + version: 0.16.45 ky: specifier: 'catalog:' - version: 1.14.3 + version: 2.0.0 lamejs: specifier: 'catalog:' version: 1.2.1 @@ -863,7 +822,7 @@ importers: version: 0.42.0 mermaid: specifier: 'catalog:' - version: 11.13.0 + version: 11.14.0 mime: specifier: 'catalog:' version: 4.1.0 @@ -875,13 +834,13 @@ importers: version: 1.0.0 next: specifier: 'catalog:' - version: 16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) + version: 16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) next-themes: specifier: 'catalog:' version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) nuqs: specifier: 'catalog:' - version: 2.8.9(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react@19.2.4) + version: 2.8.9(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react@19.2.4) pinyin-pro: specifier: 'catalog:' version: 3.28.0 @@ -908,7 +867,7 @@ importers: version: 5.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-i18next: specifier: 'catalog:' - version: 16.6.6(i18next@25.10.10(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + version: 17.0.2(i18next@26.0.3(typescript@6.0.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@6.0.2) react-multi-email: specifier: 'catalog:' version: 1.0.25(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -921,15 +880,9 @@ importers: react-sortablejs: specifier: 'catalog:' version: 6.1.4(@types/sortablejs@1.15.9)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sortablejs@1.15.7) - react-syntax-highlighter: - specifier: 'catalog:' - version: 15.6.6(react@19.2.4) react-textarea-autosize: specifier: 'catalog:' version: 8.5.9(@types/react@19.2.14)(react@19.2.4) - react-window: - specifier: 'catalog:' - version: 1.8.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4) reactflow: specifier: 'catalog:' version: 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -945,6 +898,9 @@ importers: sharp: specifier: 'catalog:' version: 0.34.5 + shiki: + specifier: 'catalog:' + version: 4.0.2 sortablejs: specifier: 'catalog:' version: 1.15.7 @@ -962,7 +918,7 @@ importers: version: 3.5.0 tldts: specifier: 'catalog:' - version: 7.0.27 + version: 7.0.28 unist-util-visit: specifier: 'catalog:' version: 5.1.0 @@ -984,10 +940,10 @@ importers: devDependencies: '@antfu/eslint-config': specifier: 'catalog:' - version: 7.7.3(@eslint-react/eslint-plugin@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(@next/eslint-plugin-next@16.2.1)(@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3))(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(@vue/compiler-sfc@3.5.31)(eslint-plugin-react-hooks@7.0.1(eslint@10.1.0(jiti@2.6.1)))(eslint-plugin-react-refresh@0.5.2(eslint@10.1.0(jiti@2.6.1)))(eslint@10.1.0(jiti@2.6.1))(oxlint@1.57.0(oxlint-tsgolint@0.17.3))(typescript@5.9.3) + version: 8.0.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@next/eslint-plugin-next@16.2.2)(@typescript-eslint/rule-tester@8.57.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@typescript-eslint/typescript-estree@8.58.1(typescript@6.0.2))(@typescript-eslint/utils@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@vue/compiler-sfc@3.5.31)(eslint-plugin-react-refresh@0.5.2(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(oxlint@1.58.0(oxlint-tsgolint@0.20.0))(typescript@6.0.2) '@chromatic-com/storybook': specifier: 'catalog:' - version: 5.1.1(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + version: 5.1.1(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) '@dify/iconify-collections': specifier: workspace:* version: link:../packages/iconify-collections @@ -996,10 +952,10 @@ importers: version: 1.9.2(tailwindcss@4.2.2) '@eslint-react/eslint-plugin': specifier: 'catalog:' - version: 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + version: 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) '@hono/node-server': specifier: 'catalog:' - version: 1.19.11(hono@4.12.9) + version: 1.19.13(hono@4.12.12) '@iconify-json/heroicons': specifier: 'catalog:' version: 1.2.3 @@ -1008,7 +964,7 @@ importers: version: 1.2.10 '@mdx-js/loader': specifier: 'catalog:' - version: 3.1.1(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) + version: 3.1.1(webpack@5.105.4(uglify-js@3.19.3)) '@mdx-js/react': specifier: 'catalog:' version: 3.1.1(@types/react@19.2.14)(react@19.2.4) @@ -1017,49 +973,49 @@ importers: version: 3.1.1(rollup@4.59.0) '@next/eslint-plugin-next': specifier: 'catalog:' - version: 16.2.1 + version: 16.2.2 '@next/mdx': specifier: 'catalog:' - version: 16.2.1(@mdx-js/loader@3.1.1(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.4)) + version: 16.2.2(@mdx-js/loader@3.1.1(webpack@5.105.4(uglify-js@3.19.3)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.4)) '@rgrove/parse-xml': specifier: 'catalog:' version: 4.2.0 '@storybook/addon-docs': specifier: 'catalog:' - version: 10.3.3(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) + version: 10.3.5(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(uglify-js@3.19.3)) '@storybook/addon-links': specifier: 'catalog:' - version: 10.3.3(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + version: 10.3.5(react@19.2.4)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) '@storybook/addon-onboarding': specifier: 'catalog:' - version: 10.3.3(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + version: 10.3.5(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) '@storybook/addon-themes': specifier: 'catalog:' - version: 10.3.3(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + version: 10.3.5(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) '@storybook/nextjs-vite': specifier: 'catalog:' - version: 10.3.3(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) + version: 10.3.5(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2)(webpack@5.105.4(uglify-js@3.19.3)) '@storybook/react': specifier: 'catalog:' - version: 10.3.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + version: 10.3.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2) '@tailwindcss/postcss': specifier: 'catalog:' version: 4.2.2 '@tailwindcss/vite': specifier: 'catalog:' - version: 4.2.2(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) + version: 4.2.2(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) '@tanstack/eslint-plugin-query': specifier: 'catalog:' - version: 5.95.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + version: 5.96.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) '@tanstack/react-devtools': specifier: 'catalog:' - version: 0.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11) + version: 0.10.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11) '@tanstack/react-form-devtools': specifier: 'catalog:' - version: 0.2.19(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11) + version: 0.2.20(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11) '@tanstack/react-query-devtools': specifier: 'catalog:' - version: 5.95.2(@tanstack/react-query@5.95.2(react@19.2.4))(react@19.2.4) + version: 5.96.2(@tanstack/react-query@5.96.2(react@19.2.4))(react@19.2.4) '@testing-library/dom': specifier: 'catalog:' version: 10.4.1 @@ -1074,13 +1030,13 @@ importers: version: 14.6.1(@testing-library/dom@10.4.1) '@tsslint/cli': specifier: 'catalog:' - version: 3.0.2(@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@5.9.3))(typescript@5.9.3) + version: 3.0.2(@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@6.0.2))(typescript@6.0.2) '@tsslint/compat-eslint': specifier: 'catalog:' - version: 3.0.2(jiti@2.6.1)(typescript@5.9.3) + version: 3.0.2(jiti@2.6.1)(typescript@6.0.2) '@tsslint/config': specifier: 'catalog:' - version: 3.0.2(@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@5.9.3))(typescript@5.9.3) + version: 3.0.2(@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@6.0.2))(typescript@6.0.2) '@types/js-cookie': specifier: 'catalog:' version: 3.0.6 @@ -1092,7 +1048,7 @@ importers: version: 0.6.4 '@types/node': specifier: 'catalog:' - version: 25.5.0 + version: 25.5.2 '@types/qs': specifier: 'catalog:' version: 6.15.0 @@ -1102,87 +1058,75 @@ importers: '@types/react-dom': specifier: 'catalog:' version: 19.2.3(@types/react@19.2.14) - '@types/react-syntax-highlighter': - specifier: 'catalog:' - version: 15.5.13 - '@types/react-window': - specifier: 'catalog:' - version: 1.8.8 '@types/sortablejs': specifier: 'catalog:' version: 1.15.9 '@typescript-eslint/parser': specifier: 'catalog:' - version: 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + version: 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260329.1 + version: 7.0.0-dev.20260407.1 '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) + version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.21(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(react@19.2.4) + version: 0.5.22(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.4) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.1(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) + version: 4.1.3(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) agentation: specifier: 'catalog:' version: 3.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) code-inspector-plugin: specifier: 'catalog:' - version: 1.4.5 + version: 1.5.1 eslint: specifier: 'catalog:' - version: 10.1.0(jiti@2.6.1) + version: 10.2.0(jiti@2.6.1) eslint-markdown: specifier: 'catalog:' - version: 0.6.0(eslint@10.1.0(jiti@2.6.1)) + version: 0.6.0(eslint@10.2.0(jiti@2.6.1)) eslint-plugin-better-tailwindcss: specifier: 'catalog:' - version: 4.3.2(eslint@10.1.0(jiti@2.6.1))(oxlint@1.57.0(oxlint-tsgolint@0.17.3))(tailwindcss@4.2.2)(typescript@5.9.3) + version: 4.3.2(eslint@10.2.0(jiti@2.6.1))(oxlint@1.58.0(oxlint-tsgolint@0.20.0))(tailwindcss@4.2.2)(typescript@6.0.2) eslint-plugin-hyoban: specifier: 'catalog:' - version: 0.14.1(eslint@10.1.0(jiti@2.6.1)) + version: 0.14.1(eslint@10.2.0(jiti@2.6.1)) eslint-plugin-markdown-preferences: specifier: 'catalog:' - version: 0.40.3(@eslint/markdown@7.5.1)(eslint@10.1.0(jiti@2.6.1)) + version: 0.41.0(@eslint/markdown@8.0.1)(eslint@10.2.0(jiti@2.6.1)) eslint-plugin-no-barrel-files: specifier: 'catalog:' - version: 1.2.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-hooks: - specifier: 'catalog:' - version: 7.0.1(eslint@10.1.0(jiti@2.6.1)) + version: 1.2.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) eslint-plugin-react-refresh: specifier: 'catalog:' - version: 0.5.2(eslint@10.1.0(jiti@2.6.1)) + version: 0.5.2(eslint@10.2.0(jiti@2.6.1)) eslint-plugin-sonarjs: specifier: 'catalog:' - version: 4.0.2(eslint@10.1.0(jiti@2.6.1)) + version: 4.0.2(eslint@10.2.0(jiti@2.6.1)) eslint-plugin-storybook: specifier: 'catalog:' - version: 10.3.3(eslint@10.1.0(jiti@2.6.1))(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + version: 10.3.5(eslint@10.2.0(jiti@2.6.1))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2) happy-dom: specifier: 'catalog:' version: 20.8.9 hono: specifier: 'catalog:' - version: 4.12.9 + version: 4.12.12 knip: specifier: 'catalog:' - version: 6.1.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + version: 6.3.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) postcss: specifier: 'catalog:' - version: 8.5.8 + version: 8.5.9 react-server-dom-webpack: specifier: 'catalog:' - version: 19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) - sass: - specifier: 'catalog:' - version: 1.98.0 + version: 19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(uglify-js@3.19.3)) storybook: specifier: 'catalog:' - version: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tailwindcss: specifier: 'catalog:' version: 4.2.2 @@ -1191,28 +1135,28 @@ importers: version: 4.21.0 typescript: specifier: 'catalog:' - version: 5.9.3 + version: 6.0.2 uglify-js: specifier: 'catalog:' version: 3.19.3 vinext: specifier: 'catalog:' - version: 0.0.38(21fde6c2677b0aab516df83ef1beed5d) + version: 0.0.40(@mdx-js/rollup@3.1.1(rollup@4.59.0))(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.22(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.4))(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.4)(typescript@6.0.2) vite: - specifier: npm:@voidzero-dev/vite-plus-core@0.1.14 - version: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + specifier: npm:@voidzero-dev/vite-plus-core@0.1.16 + version: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' vite-plugin-inspect: specifier: 'catalog:' - version: 12.0.0-beta.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)(ws@8.20.0) + version: 12.0.0-beta.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(ws@8.20.0) vite-plus: specifier: 'catalog:' - version: 0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + version: 0.1.16(@types/node@25.5.2)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) vitest: - specifier: npm:@voidzero-dev/vite-plus-test@0.1.14 - version: '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + specifier: npm:@voidzero-dev/vite-plus-test@0.1.16 + version: '@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' vitest-canvas-mock: specifier: 'catalog:' - version: 1.1.4(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) + version: 1.1.4(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) packages: @@ -1223,17 +1167,17 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@amplitude/analytics-browser@2.38.0': - resolution: {integrity: sha512-MhqyEkr1gGAR4s4GSSflDhFVheIx9Nv3FfElQu9NlNrXB2Hh3BEOyVgdK7hgfi6NJwFyfw30+t5lym+njtA8hA==} + '@amplitude/analytics-browser@2.38.1': + resolution: {integrity: sha512-8E3WDuCz5pmVysw7iwT9MjltzaO7Sqy9jWNaXovO30Z8sXs5Ncl32qv6o14kwlpl3wRSaaAKDe0Z3Grjx3dYYQ==} - '@amplitude/analytics-client-common@2.4.41': - resolution: {integrity: sha512-+GbvtvhsUROotPBwfAxbrqovKePhC0oQKXtxjbeNQleOHjBjsAs5jEOCHpJenCKtaRpucg/FuK3NVOS09MfW7Q==} + '@amplitude/analytics-client-common@2.4.42': + resolution: {integrity: sha512-pEpE6s8GsXTlD9Jj4b/wplCQD8fT2ml/VZSnQ1E5sU0goaeZaYQKMTXGpbA2aE40ABZMwQSopxJn+puBrJc8eg==} '@amplitude/analytics-connector@1.6.4': resolution: {integrity: sha512-SpIv0IQMNIq6SH3UqFGiaZyGSc7PBZwRdq7lvP0pBxW8i4Ny+8zwI0pV+VMfMHQwWY3wdIbWw5WQphNjpdq1/Q==} - '@amplitude/analytics-core@2.44.0': - resolution: {integrity: sha512-z9QuTxLqEQ8KIeAT6Vmy6K48rP9TUmjnb4GwUMYoV/fxu3B9ClTaN18zqXQMmDw9HwUiIreHiVbwTb7OQRN5aA==} + '@amplitude/analytics-core@2.44.1': + resolution: {integrity: sha512-bx8RAYneoEyT/gsCpcktEgBMUs5vIb2piA/Kof88BaNKAWEpIa9B4Ogg4vNPqmEgNIx/wztSduFMHHw2pLcncg==} '@amplitude/analytics-types@2.11.1': resolution: {integrity: sha512-wFEgb0t99ly2uJKm5oZ28Lti0Kh5RecR5XBkwfUpDzn84IoCIZ8GJTsMw/nThu8FZFc7xFDA4UAt76zhZKrs9A==} @@ -1241,26 +1185,26 @@ packages: '@amplitude/experiment-core@0.7.2': resolution: {integrity: sha512-Wc2NWvgQ+bLJLeF0A9wBSPIaw0XuqqgkPKsoNFQrmS7r5Djd56um75In05tqmVntPJZRvGKU46pAp8o5tdf4mA==} - '@amplitude/plugin-autocapture-browser@1.25.0': - resolution: {integrity: sha512-YuWsz8XmJuKu3NlMxkvlhLey/5tGCeOwwfsROHficR0yDWO9gNG0WtHl7A0Pw1PUc9iaXjqfG2AjYumAtiq16Q==} + '@amplitude/plugin-autocapture-browser@1.25.1': + resolution: {integrity: sha512-eIaPO7eUH2W0OWe0JoqUVvMPUGDeOn4JQa7zdClEbvHnPxfGS1RHIFNsBk5ofgEWxhUo2Ka/Z0Wl86k9FMaa7w==} - '@amplitude/plugin-custom-enrichment-browser@0.1.2': - resolution: {integrity: sha512-ZX9BKqs1E1OI7l7QCGu9JnB/1kqLN+zqIePgM2tuEhZNFQJaw4NhAMUaMRqvNnaCkHlmpVRISzSj/4D3tWMRtA==} + '@amplitude/plugin-custom-enrichment-browser@0.1.3': + resolution: {integrity: sha512-iKZkqkI5CpLb62cGNgvqTVEUj8i5UBFWJc0aQMZZBqc+vmzHBaqvjeAU0dwO8KA623YfT5I+/Vp1MnqvEXGJFg==} - '@amplitude/plugin-network-capture-browser@1.9.11': - resolution: {integrity: sha512-49o3zYnKUmRdrxgAEcr1iHnXR1um40e1icO0hzugSq04k19hs27zcl3zpEk9geO+nNKwO744ryE1q93gqVbHrQ==} + '@amplitude/plugin-network-capture-browser@1.9.12': + resolution: {integrity: sha512-/8x+GDqE25pTvsU9Po7Ur+V8pUuX4IG5p2xHPM9N/APfyc3D1zLTkC8FKo8wfPpg4Wu97mSzy1JnvPDqbJcJyw==} - '@amplitude/plugin-page-url-enrichment-browser@0.7.3': - resolution: {integrity: sha512-3UZq/zKg4lcsRgziWAPSEeaUsNsbyjjxmsAE9kSDi/hIj5RaWnwWhY6TGhv45UAReugTA4vVZyFRg9btf3c/Fg==} + '@amplitude/plugin-page-url-enrichment-browser@0.7.4': + resolution: {integrity: sha512-gF7V1ypkYB7FTwKlqjbO+7Z+Wvf72RfA64aREj9aplZdRJ0EY3qSEYMA3L2v0U5ztYchiy5MJraSaaxKfzXdJg==} - '@amplitude/plugin-page-view-tracking-browser@2.9.4': - resolution: {integrity: sha512-J16zmEadnzNpkHSmzpTiQN2q9pGJ/4SkHONA9O8KxUsMU/MYTDgof3rAYY/w5B5rmvdxfMRCjqWtvnkizzgZ6w==} + '@amplitude/plugin-page-view-tracking-browser@2.9.5': + resolution: {integrity: sha512-fWewMrgo0T7AyKnrZn6ox0ER5Ibw/IFTkX0GrQ8DxcsXrmUuSWUTsxZaA7YPDzuWPbd4AX9/AWZF2i6A9Ybtfg==} - '@amplitude/plugin-session-replay-browser@1.27.5': - resolution: {integrity: sha512-tf0Ty1nNF8OJ5QQ5scEqdGfzdgIaqkRf2MSzQfHbGcTIoYuVmAKuCgn3yMLk62MKnwgG3IsTIugMdRRv7l85PA==} + '@amplitude/plugin-session-replay-browser@1.27.6': + resolution: {integrity: sha512-wHv9b/Qzu9qg0thE+qo23/KpYGiADnAj42I1C1goQAJG7XNOk62F0sdejVvnQIV9NsLe0ItoS+tg3eqlBE7Exg==} - '@amplitude/plugin-web-vitals-browser@1.1.26': - resolution: {integrity: sha512-wiD4vy+f2fepr+8Lnn26TYYjDEnWsmlGhJog99x+xfbZ/D+stGdaCIOz5AOjU1TpTRvxvamEu2XuOh+8EZOCSA==} + '@amplitude/plugin-web-vitals-browser@1.1.27': + resolution: {integrity: sha512-jh/dWMsthx5E+ensNTwj7nkqi8iG8wyJc1HryOdY49w9zTgcbZmJwE2uumLBXBasn7l62a5EdqRkwctGL53fHw==} '@amplitude/rrdom@2.0.0-alpha.37': resolution: {integrity: sha512-u4dSnBtlbJ8oU5P/Ywl2RLqvjqWbkl4ScMUbvQA7in4pWcx+0NRN+VVjLZXQcd8Fn7E/rcxjeUh7e7HfwvdasQ==} @@ -1294,20 +1238,20 @@ packages: '@amplitude/rrweb@2.0.0-alpha.37': resolution: {integrity: sha512-jJkSpPYiVgOZB422pb2jOJJn3pvb5E5f9vKK8CEmUlk2mVAl6kPQzW98mb05M65OJFj5nn9tSe9h5r5+Cl93ag==} - '@amplitude/session-replay-browser@1.35.0': - resolution: {integrity: sha512-aGqu807oC8UIMmP+g1jBYsgN+/VeR/ThtK6fpxuZCugEogx7EZ9sXDEeudUmyvkQQfWmD+nLmrhYPX8FpROT5w==} + '@amplitude/session-replay-browser@1.35.1': + resolution: {integrity: sha512-7X6T+niZaG+zpvcFOwdkbTNUWzD6T9/rQ7POYkTK+C/6FtvJ0fpHXNHdHT8fozKox2UXL/wwZvoQWFriHSe1dA==} '@amplitude/targeting@0.2.0': resolution: {integrity: sha512-/50ywTrC4hfcfJVBbh5DFbqMPPfaIOivZeb5Gb+OGM03QrA+lsUqdvtnKLNuWtceD4H6QQ2KFzPJ5aAJLyzVDA==} - '@antfu/eslint-config@7.7.3': - resolution: {integrity: sha512-BtroDxTvmWtvr3yJkdWVCvwsKlnEdkreoeOyrdNezc/W5qaiQNf2xjcsQ3N5Yy0x27h+0WFfW8rG8YlVioG6dw==} + '@antfu/eslint-config@8.0.0': + resolution: {integrity: sha512-IKiCfsa1vRgj8srB2azqiN3nOAcVyP/TZ5Ibiz0TDW9NoQPizTvkmRTSi1vo4ax0SL9TH/8uJLK6uCfd6bQzLA==} hasBin: true peerDependencies: '@angular-eslint/eslint-plugin': ^21.1.0 '@angular-eslint/eslint-plugin-template': ^21.1.0 '@angular-eslint/template-parser': ^21.1.0 - '@eslint-react/eslint-plugin': ^2.11.0 + '@eslint-react/eslint-plugin': ^3.0.0 '@next/eslint-plugin-next': '>=15.0.0' '@prettier/plugin-xml': ^3.4.1 '@unocss/eslint-plugin': '>=0.50.0' @@ -1316,7 +1260,6 @@ packages: eslint-plugin-astro: ^1.2.0 eslint-plugin-format: '>=0.1.0' eslint-plugin-jsx-a11y: '>=6.10.2' - eslint-plugin-react-hooks: ^7.0.0 eslint-plugin-react-refresh: ^0.5.0 eslint-plugin-solid: ^0.14.3 eslint-plugin-svelte: '>=2.35.1' @@ -1347,8 +1290,6 @@ packages: optional: true eslint-plugin-jsx-a11y: optional: true - eslint-plugin-react-hooks: - optional: true eslint-plugin-react-refresh: optional: true eslint-plugin-solid: @@ -1367,11 +1308,6 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@antfu/ni@28.3.0': - resolution: {integrity: sha512-JbRijiCNAGcQcyPfV0EXOJYwV27e/srXfTvETqzbbh4jzHBV2pDYiBz8rj5SyzX27aTbCK+qXR3x6g2WKokcrA==} - engines: {node: '>=20'} - hasBin: true - '@antfu/utils@8.1.1': resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==} @@ -1498,32 +1434,32 @@ packages: '@clack/core@0.3.5': resolution: {integrity: sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ==} - '@clack/core@1.1.0': - resolution: {integrity: sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA==} + '@clack/core@1.2.0': + resolution: {integrity: sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg==} '@clack/prompts@0.8.2': resolution: {integrity: sha512-6b9Ab2UiZwJYA9iMyboYyW9yJvAO9V753ZhS+DHKEjZRKAxPPOb7MXXu84lsPFG+vZt6FRFniZ8rXi+zCIw4yQ==} - '@clack/prompts@1.1.0': - resolution: {integrity: sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g==} + '@clack/prompts@1.2.0': + resolution: {integrity: sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w==} - '@code-inspector/core@1.4.5': - resolution: {integrity: sha512-wskkSRX13TAqJG65d5sq0bRZ4kYktas/iE70xqXMOeqW/A6n2Zqhw5QRHANmEmlBvB9bP/bse+9iBkNN3Q2Skw==} + '@code-inspector/core@1.5.1': + resolution: {integrity: sha512-Y9JdgoxVh93xRMupTa1lT/v+UlcBEpM7Y1BTxQy924wSe6VVEXsJ1nPJ/Ob2HPMUAA6F568aHALi2KDUhA2kzg==} - '@code-inspector/esbuild@1.4.5': - resolution: {integrity: sha512-KBwq7waqZ3L1CW7N9ff7aS0HxzamrslR08i5ovkLQe1p6tH9Axe9zzCrBnvgmB0UZsT2r/5wKLOWyEpq5+VYKw==} + '@code-inspector/esbuild@1.5.1': + resolution: {integrity: sha512-Z/WZVCG6WaB9HTcDC8l15RpgEsfFj/WKLLr6cKNX/JzAYBroadLPw1N0sbUJUIQnow5cCo7KYpHrC1T27WVMnw==} - '@code-inspector/mako@1.4.5': - resolution: {integrity: sha512-yrHgE5+b4ZL29Xt+y0H/9xrXSbRskq7dFhmE9GYFWCcgdWNCMD25hZd7xZVije94++H65Vw6Bu/abfqEx0peog==} + '@code-inspector/mako@1.5.1': + resolution: {integrity: sha512-EQmqQnnyW8tf3EBRlYyRYv1n3W1PUcfaYuuXXAfBdfJIGMwJjj0PcrDsdiI5MNyFmIx3QdMREhWmPMx1LoAANg==} - '@code-inspector/turbopack@1.4.5': - resolution: {integrity: sha512-IG39ikmQthdx/oAxhpV7zsIQZ3Jpycl88JzH+UXHq0ZpfHwa1KdNc/9erP3kFMY4+ANmkmerqBk57knmRTGMRQ==} + '@code-inspector/turbopack@1.5.1': + resolution: {integrity: sha512-PeLbcDtKDoSrKPsWnwQc+Yj9KgCa3xbHxEwXa/aGVykilvfvYP9AH1z5BRyZLDgB21diSV75BPNpF+o/FQRYug==} - '@code-inspector/vite@1.4.5': - resolution: {integrity: sha512-vBtH91afwYL7JV4zWcJJTFd65LJ4SZz5E9AwGgCF30/L1mdDx7U29D+M+JpaxSgsMB6monKSZh+ubbqYe0ixpQ==} + '@code-inspector/vite@1.5.1': + resolution: {integrity: sha512-gkfmSmawYb1yDDuCft4DESXCAD3JxPt59dGiRoD78GhQzSYHk3tnLPZMH/GLBpdeFNbKHi1FtEMbAAECIJG9xg==} - '@code-inspector/webpack@1.4.5': - resolution: {integrity: sha512-lwUv+X1FNSUWz+FKcUsE2dT2pg6VFRRXKt16hg/m+Lwtdet2adfi6BFLZmNz3OPIEGbRB5Kjx6bfaghZhbDCCg==} + '@code-inspector/webpack@1.5.1': + resolution: {integrity: sha512-8i3QI/bSirORDF/0P16T6NhNy1RxO7soip8sWeV/2btLbYCwyiaDnqT4Bw3JaM8MNz0N8NaA2qItUrrKE7TtCg==} '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} @@ -1587,11 +1523,11 @@ packages: '@cucumber/tag-expressions@9.1.0': resolution: {integrity: sha512-bvHjcRFZ+J1TqIa9eFNO1wGHqwx4V9ZKV3hYgkuK/VahHx73uiP4rKV3JVrvWSMrwrFvJG6C8aEwnCWSvbyFdQ==} - '@e18e/eslint-plugin@0.2.0': - resolution: {integrity: sha512-mXgODVwhuDjTJ+UT+XSvmMmCidtGKfrV5nMIv1UtpWex2pYLsIM3RSpT8HWIMAebS9qANbXPKlSX4BE7ZvuCgA==} + '@e18e/eslint-plugin@0.3.0': + resolution: {integrity: sha512-hHgfpxsrZ2UYHcicA+tGZnmk19uJTaye9VH79O+XS8R4ona2Hx3xjhXghclNW58uXMk3xXlbYEOMr8thsoBmWg==} peerDependencies: eslint: ^9.0.0 || ^10.0.0 - oxlint: ^1.41.0 + oxlint: ^1.55.0 peerDependenciesMeta: eslint: optional: true @@ -1843,16 +1779,16 @@ packages: resolution: {integrity: sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-array@0.23.3': - resolution: {integrity: sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==} + '@eslint/config-array@0.23.4': + resolution: {integrity: sha512-lf19F24LSMfF8weXvW5QEtnLqW70u7kgit5e9PSx0MsHAFclGd1T9ynvWEMDT1w5J4Qt54tomGeAhdoAku1Xow==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/config-helpers@0.2.3': resolution: {integrity: sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.5.3': - resolution: {integrity: sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==} + '@eslint/config-helpers@0.5.4': + resolution: {integrity: sha512-jJhqiY3wPMlWWO3370M86CPJ7pt8GmEwSLglMfQhjXal07RCvhmU0as4IuUEW5SJeunfItiEetHmSxCCe9lDBg==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/core@0.14.0': @@ -1867,8 +1803,8 @@ packages: resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@1.1.1': - resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==} + '@eslint/core@1.2.0': + resolution: {integrity: sha512-8FTGbNzTvmSlc4cZBaShkC6YvFMG0riksYWRFKXztqVdXaQbcZLXlFbSpC05s70sGEsXAw0qwhx69JiW7hQS7A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/css-tree@3.6.9': @@ -1896,12 +1832,16 @@ packages: resolution: {integrity: sha512-R8uZemG9dKTbru/DQRPblbJyXpObwKzo8rv1KYGGuPUPtjM4LXBYM9q5CIZAComzZupws3tWbDwam5AFpPLyJQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/markdown@8.0.1': + resolution: {integrity: sha512-WWKmld/EyNdEB8GMq7JMPX1SDWgyJAM1uhtCi5ySrqYQM4HQjmg11EX/q3ZpnpRXHfdccFtli3NBvvGaYjWyQw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@eslint/object-schema@2.1.7': resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/object-schema@3.0.3': - resolution: {integrity: sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==} + '@eslint/object-schema@3.0.4': + resolution: {integrity: sha512-55lO/7+Yp0ISKRP0PsPtNTeNGapXaO085aELZmWCVc5SH3jfrqpuU6YgOdIxMS99ZHkQN1cXKE+cdIqwww9ptw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/plugin-kit@0.3.5': @@ -1916,6 +1856,10 @@ packages: resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@eslint/plugin-kit@0.7.0': + resolution: {integrity: sha512-ejvBr8MQCbVsWNZnCwDXjUKq40MDmHalq7cJ6e9s/qzTUFIIo/afzt1Vui9T97FM/V/pN4YsFVoed5NIa96RDg==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} @@ -1949,23 +1893,20 @@ packages: '@formatjs/intl-localematcher@0.8.2': resolution: {integrity: sha512-q05KMYGJLyqFNFtIb8NhWLF5X3aK/k0wYt7dnRFuy6aLQL+vUwQ1cg5cO4qawEiINybeCPXAWlprY2mSBjSXAQ==} - '@headlessui/react@2.2.9': - resolution: {integrity: sha512-Mb+Un58gwBn0/yWZfyrCh0TJyurtT+dETj7YHleylHk5od3dv2XqETPGWMyQ5/7sYN7oWdyM1u9MvC0OC8UmzQ==} + '@headlessui/react@2.2.10': + resolution: {integrity: sha512-5pVLNK9wlpxTUTy9GpgbX/SdcRh+HBnPktjM2wbiLTH4p+2EPHBO1aoSryUCuKUIItdDWO9ITlhUL8UnUN/oIA==} engines: {node: '>=10'} peerDependencies: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc - '@henrygd/queue@1.2.0': - resolution: {integrity: sha512-jW/BLSTpcvExDhqJGxtIPgGr2O0IFF8XUNDwEbfCfhrXT8a4xztQ9Lv6U/vbYzYC0xVWn+3zv6YnLUh3bEFUKA==} - '@heroicons/react@2.2.0': resolution: {integrity: sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==} peerDependencies: react: '>= 16 || ^19.0.0-rc' - '@hono/node-server@1.19.11': - resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} + '@hono/node-server@1.19.13': + resolution: {integrity: sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==} engines: {node: '>=18.14.1'} peerDependencies: hono: ^4 @@ -2161,11 +2102,11 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.4': - resolution: {integrity: sha512-6PyZBYKnnVNqOSB0YFly+62R7dmov8segT27A+RVTBVd4iAE6kbW9QBJGlyR2yG4D4ohzhZSTIu7BK1UTtmFFA==} + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0': + resolution: {integrity: sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ==} peerDependencies: typescript: '>= 4.3.x' - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: typescript: optional: true @@ -2285,8 +2226,8 @@ packages: peerDependencies: rollup: 4.59.0 - '@mermaid-js/parser@1.0.1': - resolution: {integrity: sha512-opmV19kN1JsK0T6HhhokHpcVkqKpF+x2pPDKKM2ThHtZAB5F4PROopk0amuVYK5qMrIA4erzpNm8gmPNJgMDxQ==} + '@mermaid-js/parser@1.1.0': + resolution: {integrity: sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==} '@monaco-editor/loader@1.7.0': resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==} @@ -2310,14 +2251,14 @@ packages: '@next/env@16.0.0': resolution: {integrity: sha512-s5j2iFGp38QsG1LWRQaE2iUY3h1jc014/melHFfLdrsMJPqxqDQwWNwyQTcNoUSGZlCVZuM7t7JDMmSyRilsnA==} - '@next/env@16.2.1': - resolution: {integrity: sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg==} + '@next/env@16.2.2': + resolution: {integrity: sha512-LqSGz5+xGk9EL/iBDr2yo/CgNQV6cFsNhRR2xhSXYh7B/hb4nePCxlmDvGEKG30NMHDFf0raqSyOZiQrO7BkHQ==} - '@next/eslint-plugin-next@16.2.1': - resolution: {integrity: sha512-r0epZGo24eT4g08jJlg2OEryBphXqO8aL18oajoTKLzHJ6jVr6P6FI58DLMug04MwD3j8Fj0YK0slyzneKVyzA==} + '@next/eslint-plugin-next@16.2.2': + resolution: {integrity: sha512-IOPbWzDQ+76AtjZioaCjpIY72xNSDMnarZ2GMQ4wjNLvnJEJHqxQwGFhgnIWLV9klb4g/+amg88Tk5OXVpyLTw==} - '@next/mdx@16.2.1': - resolution: {integrity: sha512-w0YOkOc+WEnsTJ8uxzBOvpe3R+9BnJOxWCE7qcI/62CzJiUEd8JKtF25e3R8cW5BGsKyRW8p4zE2JLyXKa8xdw==} + '@next/mdx@16.2.2': + resolution: {integrity: sha512-2CbRTXE6sJ7zDAaKXknb5FrrPs46iJeMPzuoBXsAOV/XVnxABGD4mSDusn0VuCoII/KjUZ+zsuo2VFbchYQXng==} peerDependencies: '@mdx-js/loader': '>=0.15.0' '@mdx-js/react': '>=0.15.0' @@ -2327,54 +2268,54 @@ packages: '@mdx-js/react': optional: true - '@next/swc-darwin-arm64@16.2.1': - resolution: {integrity: sha512-BwZ8w8YTaSEr2HIuXLMLxIdElNMPvY9fLqb20LX9A9OMGtJilhHLbCL3ggyd0TwjmMcTxi0XXt+ur1vWUoxj2Q==} + '@next/swc-darwin-arm64@16.2.2': + resolution: {integrity: sha512-B92G3ulrwmkDSEJEp9+XzGLex5wC1knrmCSIylyVeiAtCIfvEJYiN3v5kXPlYt5R4RFlsfO/v++aKV63Acrugg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.2.1': - resolution: {integrity: sha512-/vrcE6iQSJq3uL3VGVHiXeaKbn8Es10DGTGRJnRZlkNQQk3kaNtAJg8Y6xuAlrx/6INKVjkfi5rY0iEXorZ6uA==} + '@next/swc-darwin-x64@16.2.2': + resolution: {integrity: sha512-7ZwSgNKJNQiwW0CKhNm9B1WS2L1Olc4B2XY0hPYCAL3epFnugMhuw5TMWzMilQ3QCZcCHoYm9NGWTHbr5REFxw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.2.1': - resolution: {integrity: sha512-uLn+0BK+C31LTVbQ/QU+UaVrV0rRSJQ8RfniQAHPghDdgE+SlroYqcmFnO5iNjNfVWCyKZHYrs3Nl0mUzWxbBw==} + '@next/swc-linux-arm64-gnu@16.2.2': + resolution: {integrity: sha512-c3m8kBHMziMgo2fICOP/cd/5YlrxDU5YYjAJeQLyFsCqVF8xjOTH/QYG4a2u48CvvZZSj1eHQfBCbyh7kBr30Q==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.2.1': - resolution: {integrity: sha512-ssKq6iMRnHdnycGp9hCuGnXJZ0YPr4/wNwrfE5DbmvEcgl9+yv97/Kq3TPVDfYome1SW5geciLB9aiEqKXQjlQ==} + '@next/swc-linux-arm64-musl@16.2.2': + resolution: {integrity: sha512-VKLuscm0P/mIfzt+SDdn2+8TNNJ7f0qfEkA+az7OqQbjzKdBxAHs0UvuiVoCtbwX+dqMEL9U54b5wQ/aN3dHeg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.2.1': - resolution: {integrity: sha512-HQm7SrHRELJ30T1TSmT706IWovFFSRGxfgUkyWJZF/RKBMdbdRWJuFrcpDdE5vy9UXjFOx6L3mRdqH04Mmx0hg==} + '@next/swc-linux-x64-gnu@16.2.2': + resolution: {integrity: sha512-kU3OPHJq6sBUjOk7wc5zJ7/lipn8yGldMoAv4z67j6ov6Xo/JvzA7L7LCsyzzsXmgLEhk3Qkpwqaq/1+XpNR3g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.2.1': - resolution: {integrity: sha512-aV2iUaC/5HGEpbBkE+4B8aHIudoOy5DYekAKOMSHoIYQ66y/wIVeaRx8MS2ZMdxe/HIXlMho4ubdZs/J8441Tg==} + '@next/swc-linux-x64-musl@16.2.2': + resolution: {integrity: sha512-CKXRILyErMtUftp+coGcZ38ZwE/Aqq45VMCcRLr2I4OXKrgxIBDXHnBgeX/UMil0S09i2JXaDL3Q+TN8D/cKmg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.2.1': - resolution: {integrity: sha512-IXdNgiDHaSk0ZUJ+xp0OQTdTgnpx1RCfRTalhn3cjOP+IddTMINwA7DXZrwTmGDO8SUr5q2hdP/du4DcrB1GxA==} + '@next/swc-win32-arm64-msvc@16.2.2': + resolution: {integrity: sha512-sS/jSk5VUoShUqINJFvNjVT7JfR5ORYj/+/ZpOYbbIohv/lQfduWnGAycq2wlknbOql2xOR0DoV0s6Xfcy49+g==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.2.1': - resolution: {integrity: sha512-qvU+3a39Hay+ieIztkGSbF7+mccbbg1Tk25hc4JDylf8IHjYmY/Zm64Qq1602yPyQqvie+vf5T/uPwNxDNIoeg==} + '@next/swc-win32-x64-msvc@16.2.2': + resolution: {integrity: sha512-aHaKceJgdySReT7qeck5oShucxWRiiEuwCGK8HHALe6yZga8uyFpLkPgaRw3kkF04U7ROogL/suYCNt/+CuXGA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -2566,8 +2507,8 @@ packages: cpu: [x64] os: [win32] - '@oxc-project/runtime@0.121.0': - resolution: {integrity: sha512-p0bQukD8OEHxzY4T9OlANBbEFGnOnjo1CYi50HES7OD36UO2yPh6T+uOJKLtlg06eclxroipRCpQGMpeH8EJ/g==} + '@oxc-project/runtime@0.123.0': + resolution: {integrity: sha512-wRf0z8saz9tHLcK3YeTeBmwISrpy4bBimvKxUmryiIhbt+ZJb0nwwJNL3D8xpeWbNfZlGSlzRBZbfcbApIGZJw==} engines: {node: ^20.19.0 || >=22.12.0} '@oxc-project/types@0.121.0': @@ -2576,6 +2517,9 @@ packages: '@oxc-project/types@0.122.0': resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + '@oxc-project/types@0.123.0': + resolution: {integrity: sha512-YtECP/y8Mj1lSHiUWGSRzy/C6teUKlS87dEfuVKT09LgQbUsBW1rNg+MiJ4buGu3yuADV60gbIvo9/HplA56Ew==} + '@oxc-resolver/binding-android-arm-eabi@11.19.1': resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==} cpu: [arm] @@ -2684,276 +2628,276 @@ packages: cpu: [x64] os: [win32] - '@oxfmt/binding-android-arm-eabi@0.42.0': - resolution: {integrity: sha512-dsqPTYsozeokRjlrt/b4E7Pj0z3eS3Eg74TWQuuKbjY4VttBmA88rB7d50Xrd+TZ986qdXCNeZRPEzZHAe+jow==} + '@oxfmt/binding-android-arm-eabi@0.43.0': + resolution: {integrity: sha512-CgU2s+/9hHZgo0IxVxrbMPrMj+tJ6VM3mD7Mr/4oiz4FNTISLoCvRmB5nk4wAAle045RtRjd86m673jwPyb1OQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxfmt/binding-android-arm64@0.42.0': - resolution: {integrity: sha512-t+aAjHxcr5eOBphFHdg1ouQU9qmZZoRxnX7UOJSaTwSoKsb6TYezNKO0YbWytGXCECObRqNcUxPoPr0KaraAIg==} + '@oxfmt/binding-android-arm64@0.43.0': + resolution: {integrity: sha512-T9OfRwjA/EdYxAqbvR7TtqLv5nIrwPXuCtTwOHtS7aR9uXyn74ZYgzgTo6/ZwvTq9DY4W+DsV09hB2EXgn9EbA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxfmt/binding-darwin-arm64@0.42.0': - resolution: {integrity: sha512-ulpSEYMKg61C5bRMZinFHrKJYRoKGVbvMEXA5zM1puX3O9T6Q4XXDbft20yrDijpYWeuG59z3Nabt+npeTsM1A==} + '@oxfmt/binding-darwin-arm64@0.43.0': + resolution: {integrity: sha512-o3i49ZUSJWANzXMAAVY1wnqb65hn4JVzwlRQ5qfcwhRzIA8lGVaud31Q3by5ALHPrksp5QEaKCQF9aAS3TXpZA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxfmt/binding-darwin-x64@0.42.0': - resolution: {integrity: sha512-ttxLKhQYPdFiM8I/Ri37cvqChE4Xa562nNOsZFcv1CKTVLeEozXjKuYClNvxkXmNlcF55nzM80P+CQkdFBu+uQ==} + '@oxfmt/binding-darwin-x64@0.43.0': + resolution: {integrity: sha512-vWECzzCFkb0kK6jaHjbtC5sC3adiNWtqawFCxhpvsWlzVeKmv5bNvkB4nux+o4JKWTpHCM57NDK/MeXt44txmA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxfmt/binding-freebsd-x64@0.42.0': - resolution: {integrity: sha512-Og7QS3yI3tdIKYZ58SXik0rADxIk2jmd+/YvuHRyKULWpG4V2fR5V4hvKm624Mc0cQET35waPXiCQWvjQEjwYQ==} + '@oxfmt/binding-freebsd-x64@0.43.0': + resolution: {integrity: sha512-rgz8JpkKiI/umOf7fl9gwKyQasC8bs5SYHy6g7e4SunfLBY3+8ATcD5caIg8KLGEtKFm5ujKaH8EfjcmnhzTLg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxfmt/binding-linux-arm-gnueabihf@0.42.0': - resolution: {integrity: sha512-jwLOw/3CW4H6Vxcry4/buQHk7zm9Ne2YsidzTL1kpiMe4qqrRCwev3dkyWe2YkFmP+iZCQ7zku4KwjcLRoh8ew==} + '@oxfmt/binding-linux-arm-gnueabihf@0.43.0': + resolution: {integrity: sha512-nWYnF3vIFzT4OM1qL/HSf1Yuj96aBuKWSaObXHSWliwAk2rcj7AWd6Lf7jowEBQMo4wCZVnueIGw/7C4u0KTBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.42.0': - resolution: {integrity: sha512-XwXu2vkMtiq2h7tfvN+WA/9/5/1IoGAVCFPiiQUvcAuG3efR97KNcRGM8BetmbYouFotQ2bDal3yyjUx6IPsTg==} + '@oxfmt/binding-linux-arm-musleabihf@0.43.0': + resolution: {integrity: sha512-sFg+NWJbLfupYTF4WELHAPSnLPOn1jiDZ33Z1jfDnTaA+cC3iB35x0FMMZTFdFOz3icRIArncwCcemJFGXu6TQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.42.0': - resolution: {integrity: sha512-ea7s/XUJoT7ENAtUQDudFe3nkSM3e3Qpz4nJFRdzO2wbgXEcjnchKLEsV3+t4ev3r8nWxIYr9NRjPWtnyIFJVA==} + '@oxfmt/binding-linux-arm64-gnu@0.43.0': + resolution: {integrity: sha512-MelWqv68tX6wZEILDrTc9yewiGXe7im62+5x0bNXlCYFOZdA+VnYiJfAihbROsZ5fm90p9C3haFrqjj43XnlAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-arm64-musl@0.42.0': - resolution: {integrity: sha512-+JA0YMlSdDqmacygGi2REp57c3fN+tzARD8nwsukx9pkCHK+6DkbAA9ojS4lNKsiBjIW8WWa0pBrBWhdZEqfuw==} + '@oxfmt/binding-linux-arm64-musl@0.43.0': + resolution: {integrity: sha512-ROaWfYh+6BSJ1Arwy5ujijTlwnZetxDxzBpDc1oBR4d7rfrPBqzeyjd5WOudowzQUgyavl2wEpzn1hw3jWcqLA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-ppc64-gnu@0.42.0': - resolution: {integrity: sha512-VfnET0j4Y5mdfCzh5gBt0NK28lgn5DKx+8WgSMLYYeSooHhohdbzwAStLki9pNuGy51y4I7IoW8bqwAaCMiJQg==} + '@oxfmt/binding-linux-ppc64-gnu@0.43.0': + resolution: {integrity: sha512-PJRs/uNxmFipJJ8+SyKHh7Y7VZIKQicqrrBzvfyM5CtKi8D7yZKTwUOZV3ffxmiC2e7l1SDJpkBEOyue5NAFsg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-gnu@0.42.0': - resolution: {integrity: sha512-gVlCbmBkB0fxBWbhBj9rcxezPydsQHf4MFKeHoTSPicOQ+8oGeTQgQ8EeesSybWeiFPVRx3bgdt4IJnH6nOjAA==} + '@oxfmt/binding-linux-riscv64-gnu@0.43.0': + resolution: {integrity: sha512-j6biGAgzIhj+EtHXlbNumvwG7XqOIdiU4KgIWRXAEj/iUbHKukKW8eXa4MIwpQwW1YkxovduKtzEAPnjlnAhVQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-musl@0.42.0': - resolution: {integrity: sha512-zN5OfstL0avgt/IgvRu0zjQzVh/EPkcLzs33E9LMAzpqlLWiPWeMDZyMGFlSRGOdDjuNmlZBCgj0pFnK5u32TQ==} + '@oxfmt/binding-linux-riscv64-musl@0.43.0': + resolution: {integrity: sha512-RYWxAcslKxvy7yri24Xm9cmD0RiANaiEPs007EFG6l9h1ChM69Q5SOzACaCoz4Z9dEplnhhneeBaTWMEdpgIbA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-s390x-gnu@0.42.0': - resolution: {integrity: sha512-9X6+H2L0qMc2sCAgO9HS03bkGLMKvOFjmEdchaFlany3vNZOjnVui//D8k/xZAtQv2vaCs1reD5KAgPoIU4msA==} + '@oxfmt/binding-linux-s390x-gnu@0.43.0': + resolution: {integrity: sha512-DT6Q8zfQQy3jxpezAsBACEHNUUixKSYTwdXeXojNHe4DQOoxjPdjr3Szu6BRNjxLykZM/xMNmp9ElOIyDppwtw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-gnu@0.42.0': - resolution: {integrity: sha512-BajxJ6KQvMMdpXGPWhBGyjb2Jvx4uec0w+wi6TJZ6Tv7+MzPwe0pO8g5h1U0jyFgoaF7mDl6yKPW3ykWcbUJRw==} + '@oxfmt/binding-linux-x64-gnu@0.43.0': + resolution: {integrity: sha512-R8Yk7iYcuZORXmCfFZClqbDxRZgZ9/HEidUuBNdoX8Ptx07cMePnMVJ/woB84lFIDjh2ROHVaOP40Ds3rBXFqg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-musl@0.42.0': - resolution: {integrity: sha512-0wV284I6vc5f0AqAhgAbHU2935B4bVpncPoe5n/WzVZY/KnHgqxC8iSFGeSyLWEgstFboIcWkOPck7tqbdHkzA==} + '@oxfmt/binding-linux-x64-musl@0.43.0': + resolution: {integrity: sha512-F2YYqyvnQNvi320RWZNAvsaWEHwmW3k4OwNJ1hZxRKXupY63expbBaNp6jAgvYs7y/g546vuQnGHQuCBhslhLQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxfmt/binding-openharmony-arm64@0.42.0': - resolution: {integrity: sha512-p4BG6HpGnhfgHk1rzZfyR6zcWkE7iLrWxyehHfXUy4Qa5j3e0roglFOdP/Nj5cJJ58MA3isQ5dlfkW2nNEpolw==} + '@oxfmt/binding-openharmony-arm64@0.43.0': + resolution: {integrity: sha512-OE6TdietLXV3F6c7pNIhx/9YC1/2YFwjU9DPc/fbjxIX19hNIaP1rS0cFjCGJlGX+cVJwIKWe8Mos+LdQ1yAJw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxfmt/binding-win32-arm64-msvc@0.42.0': - resolution: {integrity: sha512-mn//WV60A+IetORDxYieYGAoQso4KnVRRjORDewMcod4irlRe0OSC7YPhhwaexYNPQz/GCFk+v9iUcZ2W22yxQ==} + '@oxfmt/binding-win32-arm64-msvc@0.43.0': + resolution: {integrity: sha512-0nWK6a7pGkbdoypfVicmV9k/N1FwjPZENoqhlTU+5HhZnAhpIO3za30nEE33u6l6tuy9OVfpdXUqxUgZ+4lbZw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.42.0': - resolution: {integrity: sha512-3gWltUrvuz4LPJXWivoAxZ28Of2O4N7OGuM5/X3ubPXCEV8hmgECLZzjz7UYvSDUS3grfdccQwmjynm+51EFpw==} + '@oxfmt/binding-win32-ia32-msvc@0.43.0': + resolution: {integrity: sha512-9aokTR4Ft+tRdvgN/pKzSkVy2ksc4/dCpDm9L/xFrbIw0yhLtASLbvoG/5WOTUh/BRPPnfGTsWznEqv0dlOmhA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxfmt/binding-win32-x64-msvc@0.42.0': - resolution: {integrity: sha512-Wg4TMAfQRL9J9AZevJ/ZNy3uyyDztDYQtGr4P8UyyzIhLhFrdSmz1J/9JT+rv0fiCDLaFOBQnj3f3K3+a5PzDQ==} + '@oxfmt/binding-win32-x64-msvc@0.43.0': + resolution: {integrity: sha512-4bPgdQux2ZLWn3bf2TTXXMHcJB4lenmuxrLqygPmvCJ104Yqzj1UctxSRzR31TiJ4MLaG22RK8dUsVpJtrCz5g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxlint-tsgolint/darwin-arm64@0.17.3': - resolution: {integrity: sha512-5aDl4mxXWs+Bj02pNrX6YY6v9KMZjLIytXoqolLEo0dfBNVeZUonZgJAa/w0aUmijwIRrBhxEzb42oLuUtfkGw==} + '@oxlint-tsgolint/darwin-arm64@0.20.0': + resolution: {integrity: sha512-KKQcIHZHMxqpHUA1VXIbOG6chNCFkUWbQy6M+AFVtPKkA/3xAeJkJ3njoV66bfzwPHRcWQO+kcj5XqtbkjakoA==} cpu: [arm64] os: [darwin] - '@oxlint-tsgolint/darwin-x64@0.17.3': - resolution: {integrity: sha512-gPBy4DS5ueCgXzko20XsNZzDe/Cxde056B+QuPLGvz05CGEAtmRfpImwnyY2lAXXjPL+SmnC/OYexu8zI12yHQ==} + '@oxlint-tsgolint/darwin-x64@0.20.0': + resolution: {integrity: sha512-7HeVMuclGfG+NLZi2ybY0T4fMI7/XxO/208rJk+zEIloKkVnlh11Wd241JMGwgNFXn+MLJbOqOfojDb2Dt4L1g==} cpu: [x64] os: [darwin] - '@oxlint-tsgolint/linux-arm64@0.17.3': - resolution: {integrity: sha512-+pkunvCfB6pB0G9qHVVXUao3nqzXQPo4O3DReIi+5nGa+bOU3J3Srgy+Zb8VyOL+WDsSMJ+U7+r09cKHWhz3hg==} + '@oxlint-tsgolint/linux-arm64@0.20.0': + resolution: {integrity: sha512-zxhUwz+WSxE6oWlZLK2z2ps9yC6ebmgoYmjAl0Oa48+GqkZ56NVgo+wb8DURNv6xrggzHStQxqQxe3mK51HZag==} cpu: [arm64] os: [linux] - '@oxlint-tsgolint/linux-x64@0.17.3': - resolution: {integrity: sha512-/kW5oXtBThu4FjmgIBthdmMjWLzT3M1TEDQhxDu7hQU5xDeTd60CDXb2SSwKCbue9xu7MbiFoJu83LN0Z/d38g==} + '@oxlint-tsgolint/linux-x64@0.20.0': + resolution: {integrity: sha512-/1l6FnahC9im8PK+Ekkx/V3yetO/PzZnJegE2FXcv/iXEhbeVxP/ouiTYcUQu9shT1FWJCSNti1VJHH+21Y1dg==} cpu: [x64] os: [linux] - '@oxlint-tsgolint/win32-arm64@0.17.3': - resolution: {integrity: sha512-NMELRvbz4Ed4dxg8WiqZxtu3k4OJEp2B9KInZW+BMfqEqbwZdEJY83tbqz2hD1EjKO2akrqBQ0GpRUJEkd8kKw==} + '@oxlint-tsgolint/win32-arm64@0.20.0': + resolution: {integrity: sha512-oPZ5Yz8sVdo7P/5q+i3IKeix31eFZ55JAPa1+RGPoe9PoaYVsdMvR6Jvib6YtrqoJnFPlg3fjEjlEPL8VBKYJA==} cpu: [arm64] os: [win32] - '@oxlint-tsgolint/win32-x64@0.17.3': - resolution: {integrity: sha512-+pJ7r8J3SLPws5uoidVplZc8R/lpKyKPE6LoPGv9BME00Y1VjT6jWGx/dtUN8PWvcu3iTC6k+8u3ojFSJNmWTg==} + '@oxlint-tsgolint/win32-x64@0.20.0': + resolution: {integrity: sha512-4stx8RHj3SP9vQyRF/yZbz5igtPvYMEUR8CUoha4BVNZihi39DpCR8qkU7lpjB5Ga1DRMo2pHaA4bdTOMaY4mw==} cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.57.0': - resolution: {integrity: sha512-C7EiyfAJG4B70496eV543nKiq5cH0o/xIh/ufbjQz3SIvHhlDDsyn+mRFh+aW8KskTyUpyH2LGWL8p2oN6bl1A==} + '@oxlint/binding-android-arm-eabi@1.58.0': + resolution: {integrity: sha512-1T7UN3SsWWxpWyWGn1cT3ASNJOo+pI3eUkmEl7HgtowapcV8kslYpFQcYn431VuxghXakPNlbjRwhqmR37PFOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.57.0': - resolution: {integrity: sha512-9i80AresjZ/FZf5xK8tKFbhQnijD4s1eOZw6/FHUwD59HEZbVLRc2C88ADYJfLZrF5XofWDiRX/Ja9KefCLy7w==} + '@oxlint/binding-android-arm64@1.58.0': + resolution: {integrity: sha512-GryzujxuiRv2YFF7bRy8mKcxlbuAN+euVUtGJt9KKbLT8JBUIosamVhcthLh+VEr6KE6cjeVMAQxKAzJcoN7dg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.57.0': - resolution: {integrity: sha512-0eUfhRz5L2yKa9I8k3qpyl37XK3oBS5BvrgdVIx599WZK63P8sMbg+0s4IuxmIiZuBK68Ek+Z+gcKgeYf0otsg==} + '@oxlint/binding-darwin-arm64@1.58.0': + resolution: {integrity: sha512-7/bRSJIwl4GxeZL9rPZ11anNTyUO9epZrfEJH/ZMla3+/gbQ6xZixh9nOhsZ0QwsTW7/5J2A/fHbD1udC5DQQA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.57.0': - resolution: {integrity: sha512-UvrSuzBaYOue+QMAcuDITe0k/Vhj6KZGjfnI6x+NkxBTke/VoM7ZisaxgNY0LWuBkTnd1OmeQfEQdQ48fRjkQg==} + '@oxlint/binding-darwin-x64@1.58.0': + resolution: {integrity: sha512-EqdtJSiHweS2vfILNrpyJ6HUwpEq2g7+4Zx1FPi4hu3Hu7tC3znF6ufbXO8Ub2LD4mGgznjI7kSdku9NDD1Mkg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.57.0': - resolution: {integrity: sha512-wtQq0dCoiw4bUwlsNVDJJ3pxJA218fOezpgtLKrbQqUtQJcM9yP8z+I9fu14aHg0uyAxIY+99toL6uBa2r7nxA==} + '@oxlint/binding-freebsd-x64@1.58.0': + resolution: {integrity: sha512-VQt5TH4M42mY20F545G637RKxV/yjwVtKk2vfXuazfReSIiuvWBnv+FVSvIV5fKVTJNjt3GSJibh6JecbhGdBw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.57.0': - resolution: {integrity: sha512-qxFWl2BBBFcT4djKa+OtMdnLgoHEJXpqjyGwz8OhW35ImoCwR5qtAGqApNYce5260FQqoAHW8S8eZTjiX67Tsg==} + '@oxlint/binding-linux-arm-gnueabihf@1.58.0': + resolution: {integrity: sha512-fBYcj4ucwpAtjJT3oeBdFBYKvNyjRSK+cyuvBOTQjh0jvKp4yeA4S/D0IsCHus/VPaNG5L48qQkh+Vjy3HL2/Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.57.0': - resolution: {integrity: sha512-SQoIsBU7J0bDW15/f0/RvxHfY3Y0+eB/caKBQtNFbuerTiA6JCYx9P1MrrFTwY2dTm/lMgTSgskvCEYk2AtG/Q==} + '@oxlint/binding-linux-arm-musleabihf@1.58.0': + resolution: {integrity: sha512-0BeuFfwlUHlJ1xpEdSD1YO3vByEFGPg36uLjK1JgFaxFb4W6w17F8ET8sz5cheZ4+x5f2xzdnRrrWv83E3Yd8g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.57.0': - resolution: {integrity: sha512-jqxYd1W6WMeozsCmqe9Rzbu3SRrGTyGDAipRlRggetyYbUksJqJKvUNTQtZR/KFoJPb+grnSm5SHhdWrywv3RQ==} + '@oxlint/binding-linux-arm64-gnu@1.58.0': + resolution: {integrity: sha512-TXlZgnPTlxrQzxG9ZXU7BNwx1Ilrr17P3GwZY0If2EzrinqRH3zXPc3HrRcBJgcsoZNMuNL5YivtkJYgp467UQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.57.0': - resolution: {integrity: sha512-i66WyEPVEvq9bxRUCJ/MP5EBfnTDN3nhwEdFZFTO5MmLLvzngfWEG3NSdXQzTT3vk5B9i6C2XSIYBh+aG6uqyg==} + '@oxlint/binding-linux-arm64-musl@1.58.0': + resolution: {integrity: sha512-zSoYRo5dxHLcUx93Stl2hW3hSNjPt99O70eRVWt5A1zwJ+FPjeCCANCD2a9R4JbHsdcl11TIQOjyigcRVOH2mw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.57.0': - resolution: {integrity: sha512-oMZDCwz4NobclZU3pH+V1/upVlJZiZvne4jQP+zhJwt+lmio4XXr4qG47CehvrW1Lx2YZiIHuxM2D4YpkG3KVA==} + '@oxlint/binding-linux-ppc64-gnu@1.58.0': + resolution: {integrity: sha512-NQ0U/lqxH2/VxBYeAIvMNUK1y0a1bJ3ZicqkF2c6wfakbEciP9jvIE4yNzCFpZaqeIeRYaV7AVGqEO1yrfVPjA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.57.0': - resolution: {integrity: sha512-uoBnjJ3MMEBbfnWC1jSFr7/nSCkcQYa72NYoNtLl1imshDnWSolYCjzb8LVCwYCCfLJXD+0gBLD7fyC14c0+0g==} + '@oxlint/binding-linux-riscv64-gnu@1.58.0': + resolution: {integrity: sha512-X9J+kr3gIC9FT8GuZt0ekzpNUtkBVzMVU4KiKDSlocyQuEgi3gBbXYN8UkQiV77FTusLDPsovjo95YedHr+3yg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.57.0': - resolution: {integrity: sha512-BdrwD7haPZ8a9KrZhKJRSj6jwCor+Z8tHFZ3PT89Y3Jq5v3LfMfEePeAmD0LOTWpiTmzSzdmyw9ijneapiVHKQ==} + '@oxlint/binding-linux-riscv64-musl@1.58.0': + resolution: {integrity: sha512-CDze3pi1OO3Wvb/QsXjmLEY4XPKGM6kIo82ssNOgmcl1IdndF9VSGAE38YLhADWmOac7fjqhBw82LozuUVxD0Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.57.0': - resolution: {integrity: sha512-BNs+7ZNsRstVg2tpNxAXfMX/Iv5oZh204dVyb8Z37+/gCh+yZqNTlg6YwCLIMPSk5wLWIGOaQjT0GUOahKYImw==} + '@oxlint/binding-linux-s390x-gnu@1.58.0': + resolution: {integrity: sha512-b/89glbxFaEAcA6Uf1FvCNecBJEgcUTsV1quzrqXM/o4R1M4u+2KCVuyGCayN2UpsRWtGGLb+Ver0tBBpxaPog==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.57.0': - resolution: {integrity: sha512-AghS18w+XcENcAX0+BQGLiqjpqpaxKJa4cWWP0OWNLacs27vHBxu7TYkv9LUSGe5w8lOJHeMxcYfZNOAPqw2bg==} + '@oxlint/binding-linux-x64-gnu@1.58.0': + resolution: {integrity: sha512-0/yYpkq9VJFCEcuRlrViGj8pJUFFvNS4EkEREaN7CB1EcLXJIaVSSa5eCihwBGXtOZxhnblWgxks9juRdNQI7w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.57.0': - resolution: {integrity: sha512-E/FV3GB8phu/Rpkhz5T96hAiJlGzn91qX5yj5gU754P5cmVGXY1Jw/VSjDSlZBCY3VHjsVLdzgdkJaomEmcNOg==} + '@oxlint/binding-linux-x64-musl@1.58.0': + resolution: {integrity: sha512-hr6FNvmcAXiH+JxSvaJ4SJ1HofkdqEElXICW9sm3/Rd5eC3t7kzvmLyRAB3NngKO2wzXRCAm4Z/mGWfrsS4X8w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.57.0': - resolution: {integrity: sha512-xvZ2yZt0nUVfU14iuGv3V25jpr9pov5N0Wr28RXnHFxHCRxNDMtYPHV61gGLhN9IlXM96gI4pyYpLSJC5ClLCQ==} + '@oxlint/binding-openharmony-arm64@1.58.0': + resolution: {integrity: sha512-R+O368VXgRql1K6Xar+FEo7NEwfo13EibPMoTv3sesYQedRXd6m30Dh/7lZMxnrQVFfeo4EOfYIP4FpcgWQNHg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.57.0': - resolution: {integrity: sha512-Z4D8Pd0AyHBKeazhdIXeUUy5sIS3Mo0veOlzlDECg6PhRRKgEsBJCCV1n+keUZtQ04OP+i7+itS3kOykUyNhDg==} + '@oxlint/binding-win32-arm64-msvc@1.58.0': + resolution: {integrity: sha512-Q0FZiAY/3c4YRj4z3h9K1PgaByrifrfbBoODSeX7gy97UtB7pySPUQfC2B/GbxWU6k7CzQrRy5gME10PltLAFQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.57.0': - resolution: {integrity: sha512-StOZ9nFMVKvevicbQfql6Pouu9pgbeQnu60Fvhz2S6yfMaii+wnueLnqQ5I1JPgNF0Syew4voBlAaHD13wH6tw==} + '@oxlint/binding-win32-ia32-msvc@1.58.0': + resolution: {integrity: sha512-Y8FKBABrSPp9H0QkRLHDHOSUgM/309a3IvOVgPcVxYcX70wxJrk608CuTg7w+C6vEd724X5wJoNkBcGYfH7nNQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.57.0': - resolution: {integrity: sha512-6PuxhYgth8TuW0+ABPOIkGdBYw+qYGxgIdXPHSVpiCDm+hqTTWCmC739St1Xni0DJBt8HnSHTG67i1y6gr8qrA==} + '@oxlint/binding-win32-x64-msvc@1.58.0': + resolution: {integrity: sha512-bCn5rbiz5My+Bj7M09sDcnqW0QJyINRVxdZ65x1/Y2tGrMwherwK/lpk+HRQCKvXa8pcaQdF5KY5j54VGZLwNg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -3050,8 +2994,8 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.58.2': - resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + '@playwright/test@1.59.1': + resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} engines: {node: '>=18'} hasBin: true @@ -3061,9 +3005,6 @@ packages: '@preact/signals-core@1.14.0': resolution: {integrity: sha512-AowtCcCU/33lFlh1zRFf/u+12rfrhtNakj7UpaGEsmMwUKpKWMVvcktOGcwBBNiB4lWrZWc01LhiyyzVklJyaQ==} - '@quansync/fs@1.0.0': - resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} - '@radix-ui/primitive@1.1.3': resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} @@ -3441,8 +3382,8 @@ packages: '@rolldown/pluginutils@1.0.0-rc.12': resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} - '@rolldown/pluginutils@1.0.0-rc.5': - resolution: {integrity: sha512-RxlLX/DPoarZ9PtxVrQgZhPoor987YtKQqCo5zkjX+0S0yLJ7Vv515Wk6+xtTL67VONKJKxETWZwuZjss2idYw==} + '@rolldown/pluginutils@1.0.0-rc.13': + resolution: {integrity: sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==} '@rolldown/pluginutils@1.0.0-rc.7': resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} @@ -3603,36 +3544,67 @@ packages: cpu: [x64] os: [win32] - '@sentry-internal/browser-utils@10.46.0': - resolution: {integrity: sha512-WB1gBT9G13V02ekZ6NpUhoI1aGHV2eNfjEPthkU2bGBvFpQKnstwzjg7waIRGR7cu+YSW2Q6UI6aQLgBeOPD1g==} + '@sentry-internal/browser-utils@10.47.0': + resolution: {integrity: sha512-bVFRAeJWMBcBCvJKIFCMJ1/yQToL4vPGqfmlnDZeypcxkqUDKQ/Y3ziLHXoDL2sx0lagcgU2vH1QhCQ67Aujjw==} engines: {node: '>=18'} - '@sentry-internal/feedback@10.46.0': - resolution: {integrity: sha512-c4pI/z9nZCQXe9GYEw/hE/YTY9AxGBp8/wgKI+T8zylrN35SGHaXv63szzE1WbI8lacBY8lBF7rstq9bQVCaHw==} + '@sentry-internal/feedback@10.47.0': + resolution: {integrity: sha512-pdvMmi4dQpX5S/vAAzrhHPIw3T3HjUgDNgUiCBrlp7N9/6zGO2gNPhUnNekP+CjgI/z0rvf49RLqlDenpNrMOg==} engines: {node: '>=18'} - '@sentry-internal/replay-canvas@10.46.0': - resolution: {integrity: sha512-ub314MWUsekVCuoH0/HJbbimlI24SkV745UW2pj9xRbxOAEf1wjkmIzxKrMDbTgJGuEunug02XZVdJFJUzOcDw==} + '@sentry-internal/replay-canvas@10.47.0': + resolution: {integrity: sha512-A5OY8friSe6g8WAK4L8IeOPiEd9D3Ps40DzRH5j2f6SUja0t90mKMvHRcRf8zq0d4BkdB+JM7tjOkwxpuv8heA==} engines: {node: '>=18'} - '@sentry-internal/replay@10.46.0': - resolution: {integrity: sha512-JBsWeXG6bRbxBFK8GzWymWGOB9QE7Kl57BeF3jzgdHTuHSWZ2mRnAmb1K05T4LU+gVygk6yW0KmdC8Py9Qzg9A==} + '@sentry-internal/replay@10.47.0': + resolution: {integrity: sha512-ScdovxP7hJxgMt70+7hFvwT02GIaIUAxdEM/YPsayZBeCoAukPW8WiwztJfoKtsfPyKJ5A6f0H3PIxTPcA9Row==} engines: {node: '>=18'} - '@sentry/browser@10.46.0': - resolution: {integrity: sha512-80DmGlTk5Z2/OxVOzLNxwolMyouuAYKqG8KUcoyintZqHbF6kO1RulI610HmyUt3OagKeBCqt9S7w0VIfCRL+Q==} + '@sentry/browser@10.47.0': + resolution: {integrity: sha512-rC0agZdxKA5XWfL4VwPOr/rJMogXDqZgnVzr93YWpFn9DMZT/7LzxSJVPIJwRUjx3bFEby3PcTa3YaX7pxm1AA==} engines: {node: '>=18'} - '@sentry/core@10.46.0': - resolution: {integrity: sha512-N3fj4zqBQOhXliS1Ne9euqIKuciHCGOJfPGQLwBoW9DNz03jF+NB8+dUKtrJ79YLoftjVgf8nbgwtADK7NR+2Q==} + '@sentry/core@10.47.0': + resolution: {integrity: sha512-nsYRAx3EWezDut+Zl+UwwP07thh9uY7CfSAi2whTdcJl5hu1nSp2z8bba7Vq/MGbNLnazkd3A+GITBEML924JA==} engines: {node: '>=18'} - '@sentry/react@10.46.0': - resolution: {integrity: sha512-Rb1S+9OuUPVwsz7GWnQ6Kgf3azbsseUymIegg3JZHNcW/fM1nPpaljzTBnuineia113DH0pgMBcdrrZDLaosFQ==} + '@sentry/react@10.47.0': + resolution: {integrity: sha512-ZtJV6xxF8jUVE9e3YQUG3Do0XapG1GjniyLyqMPgN6cNvs/HaRJODf7m60By+VGqcl5XArEjEPTvx8CdPUXDfA==} engines: {node: '>=18'} peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x + '@shikijs/core@4.0.2': + resolution: {integrity: sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==} + engines: {node: '>=20'} + + '@shikijs/engine-javascript@4.0.2': + resolution: {integrity: sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==} + engines: {node: '>=20'} + + '@shikijs/engine-oniguruma@4.0.2': + resolution: {integrity: sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==} + engines: {node: '>=20'} + + '@shikijs/langs@4.0.2': + resolution: {integrity: sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==} + engines: {node: '>=20'} + + '@shikijs/primitive@4.0.2': + resolution: {integrity: sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==} + engines: {node: '>=20'} + + '@shikijs/themes@4.0.2': + resolution: {integrity: sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==} + engines: {node: '>=20'} + + '@shikijs/types@4.0.2': + resolution: {integrity: sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==} + engines: {node: '>=20'} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@shuding/opentype.js@1.4.0-beta.0': resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} engines: {node: '>= 8.0.0'} @@ -3678,42 +3650,42 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@storybook/addon-docs@10.3.3': - resolution: {integrity: sha512-trJQTpOtuOEuNv1Rn8X2Sopp5hSPpb0u0soEJ71BZAbxe4d2Y1d/1MYcxBdRKwncum6sCTsnxTpqQ/qvSJKlTQ==} + '@storybook/addon-docs@10.3.5': + resolution: {integrity: sha512-WuHbxia/o5TX4Rg/IFD0641K5qId/Nk0dxhmAUNoFs5L0+yfZUwh65XOBbzXqrkYmYmcVID4v7cgDRmzstQNkA==} peerDependencies: - storybook: ^10.3.3 + storybook: ^10.3.5 - '@storybook/addon-links@10.3.3': - resolution: {integrity: sha512-tazBHlB+YbU62bde5DWsq0lnxZjcAsPB3YRUpN2hSMfAySsudRingyWrgu5KeOxXhJvKJj0ohjQvGcMx/wgQUA==} + '@storybook/addon-links@10.3.5': + resolution: {integrity: sha512-Xe2wCGZ+hpZ0cDqAIBHk+kPc8nODNbu585ghd5bLrlYJMDVXoNM/fIlkrLgjIDVbfpgeJLUEg7vldJrn+FyOLw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.3 + storybook: ^10.3.5 peerDependenciesMeta: react: optional: true - '@storybook/addon-onboarding@10.3.3': - resolution: {integrity: sha512-HZiHfXdcLc29WkYFW+1VAMtJCeAZOOLRYPvs97woJUcZqW8yfWEJ9MWH+j++736SFAv2aqZWNmP47OdBJ/kMkw==} + '@storybook/addon-onboarding@10.3.5': + resolution: {integrity: sha512-s3/gIy9Tqxji27iclLY+KSk8kGeow1JxXMl1lPLyu8n6XVvv+tFrUPhAvUTs+fVenG6JQEWc0uzpYBdFRWbMtw==} peerDependencies: - storybook: ^10.3.3 + storybook: ^10.3.5 - '@storybook/addon-themes@10.3.3': - resolution: {integrity: sha512-6PgH1o7yNnWRVj4lAT1DNcX/eZXKgzjhfmzgWh3oFpPfDDvUzpFxx+MClM5f/ZieIbyQscxEuq8li7+e/F5VEQ==} + '@storybook/addon-themes@10.3.5': + resolution: {integrity: sha512-Mv+C7GuZ0MhGRx5C+rv8sCEjgYsDTLBvq68101V0s8Vwh3gKd6W9cbS31HoOeLAiIMiPPZ8C1iWudA3Oumdtlw==} peerDependencies: - storybook: ^10.3.3 + storybook: ^10.3.5 - '@storybook/builder-vite@10.3.3': - resolution: {integrity: sha512-awspKCTZvXyeV3KabL0id62mFbxR5u/5yyGQultwCiSb2/yVgBfip2MAqLyS850pvTiB6QFVM9deOyd2/G/bEA==} + '@storybook/builder-vite@10.3.5': + resolution: {integrity: sha512-i4KwCOKbhtlbQIbhm53+Kk7bMnxa0cwTn1pxmtA/x5wm1Qu7FrrBQV0V0DNjkUqzcSKo1CjspASJV/HlY0zYlw==} peerDependencies: - storybook: ^10.3.3 + storybook: ^10.3.5 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@storybook/csf-plugin@10.3.3': - resolution: {integrity: sha512-Utlh7zubm+4iOzBBfzLW4F4vD99UBtl2Do4edlzK2F7krQIcFvR2ontjAE8S1FQVLZAC3WHalCOS+Ch8zf3knA==} + '@storybook/csf-plugin@10.3.5': + resolution: {integrity: sha512-qlEzNKxOjq86pvrbuMwiGD/bylnsXk1dg7ve0j77YFjEEchqtl7qTlrXvFdNaLA89GhW6D/EV6eOCu/eobPDgw==} peerDependencies: esbuild: 0.27.2 rollup: 4.59.0 - storybook: ^10.3.3 + storybook: ^10.3.5 vite: '*' webpack: '*' peerDependenciesMeta: @@ -3735,40 +3707,40 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@storybook/nextjs-vite@10.3.3': - resolution: {integrity: sha512-/OzOo0dSd0eFIAF9ft+ptwaXHa5Xj01cw3NXEtmPdODZXl0eiPmTvWYIJeP26UEPzI2FFSm4fK64ZZJluKpGOA==} + '@storybook/nextjs-vite@10.3.5': + resolution: {integrity: sha512-PdgekGAnr4m/xhrvtl+ZVh68vKTfJN/AewxmqxqxSlwk0dO7B+uVGjO79WmEZwIlLvdT+3HIThTEfC1ozfpM7A==} peerDependencies: next: ^14.1.0 || ^15.0.0 || ^16.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.3 + storybook: ^10.3.5 typescript: '*' vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: typescript: optional: true - '@storybook/react-dom-shim@10.3.3': - resolution: {integrity: sha512-lkhuh4G3UTreU9M3Iz5Dt32c6U+l/4XuvqLtbe1sDHENZH6aPj7y0b5FwnfHyvuTvYRhtbo29xZrF5Bp9kCC0w==} + '@storybook/react-dom-shim@10.3.5': + resolution: {integrity: sha512-Gw8R7XZm0zSUH0XAuxlQJhmizsLzyD6x00KOlP6l7oW9eQHXGfxg3seNDG3WrSAcW07iP1/P422kuiriQlOv7g==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.3 + storybook: ^10.3.5 - '@storybook/react-vite@10.3.3': - resolution: {integrity: sha512-qHdlBe1hjqFAGXa8JL7bWTLbP/gDqXbWDm+SYCB646NHh5yvVDkZLwigP5Y+UL7M2ASfqFtosnroUK9tcCM2dw==} + '@storybook/react-vite@10.3.5': + resolution: {integrity: sha512-UB5sJHeh26bfd8sNMx2YPGYRYmErIdTRaLOT28m4bykQIa1l9IgVktsYg/geW7KsJU0lXd3oTbnUjLD+enpi3w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.3 + storybook: ^10.3.5 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@storybook/react@10.3.3': - resolution: {integrity: sha512-cGG5TbR8Tdx9zwlpsWyBEfWrejm5iWdYF26EwIhwuKq9GFUTAVrQzo0Rs7Tqc3ZyVhRS/YfsRiWSEH+zmq2JiQ==} + '@storybook/react@10.3.5': + resolution: {integrity: sha512-tpLTLaVGoA6fLK3ReyGzZUricq7lyPaV2hLPpj5wqdXLV/LpRtAHClUpNoPDYSBjlnSjL81hMZijbkGC3mA+gw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.3 + storybook: ^10.3.5 typescript: '>= 4.9.x' peerDependenciesMeta: typescript: @@ -3971,15 +3943,15 @@ packages: vue: optional: true - '@tanstack/devtools@0.11.0': - resolution: {integrity: sha512-ARRAnEm0HYjKlB2adC9YyDG3fbq5LVjpxPe6Jz583SanXRM1aKrZIGHIA//oRldX3mWIpM4kB6mCyd+CXCLqhA==} + '@tanstack/devtools@0.11.2': + resolution: {integrity: sha512-K8+tsBx+ptTLqqd4dOF10B6laj1g+XYImqYZL9n0jBINGaT+sOf17PKV9pbBt8kdbZeIGsHaJ5OZWCyZoHqN4A==} engines: {node: '>=18'} hasBin: true peerDependencies: solid-js: 1.9.11 - '@tanstack/eslint-plugin-query@5.95.2': - resolution: {integrity: sha512-EYUFRaqjBep4EHMPpZR12sXP7Kr5qv9iDIlq93NfbhHwhITaW6Txu3ROO6dLFz5r84T8p+oZXBG77pa2Wuok7A==} + '@tanstack/eslint-plugin-query@5.96.2': + resolution: {integrity: sha512-OsXCATZ+YmG8TyHrunfYy2IDB+dqY87en2im2A60JPgDAg66cCoHTzJWbe9uH8Cw9/K3NiKYlyyo1erVFu3qFw==} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ^5.4.0 @@ -3987,11 +3959,11 @@ packages: typescript: optional: true - '@tanstack/form-core@1.28.5': - resolution: {integrity: sha512-8lYnduHHfP6uaXF9+2OLnh3Fo27tH4TdtekWLG2b/Bp26ynbrWG6L4qhBgEb7VcvTpJw/RjvJF/JyFhZkG3pfQ==} + '@tanstack/form-core@1.28.6': + resolution: {integrity: sha512-4zroxL6VDj5O+w7l3dYZnUeL/h30KtNSV7UWzKAL7cl+8clMFdISPDlDlluS37As7oqvPVKo8B83VlIBvgmRog==} - '@tanstack/form-devtools@0.2.19': - resolution: {integrity: sha512-AmIq5MBcop+gYKFutowGU7py9idorJkp4a4lsR2ZIZ5qa4ekl4jWqj6Vu+kvRPpJiBl3QpiFbm9bjBvO2DueFA==} + '@tanstack/form-devtools@0.2.20': + resolution: {integrity: sha512-4cW/eU5DBTrWP53mxwHKp4NQWTIQ3XCA91pMWK7dFNNClIwFnxoSJoKwyUa6b8kRIO6uq1Sjk2mhkAtj5kB22A==} peerDependencies: solid-js: 1.9.11 @@ -3999,14 +3971,14 @@ packages: resolution: {integrity: sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==} engines: {node: '>=18'} - '@tanstack/query-core@5.95.2': - resolution: {integrity: sha512-o4T8vZHZET4Bib3jZ/tCW9/7080urD4c+0/AUaYVpIqOsr7y0reBc1oX3ttNaSW5mYyvZHctiQ/UOP2PfdmFEQ==} + '@tanstack/query-core@5.96.2': + resolution: {integrity: sha512-hzI6cTVh4KNRk8UtoIBS7Lv9g6BnJPXvBKsvYH1aGWvv0347jT3BnSvztOE+kD76XGvZnRC/t6qdW1CaIfwCeA==} - '@tanstack/query-devtools@5.95.2': - resolution: {integrity: sha512-QfaoqBn9uAZ+ICkA8brd1EHj+qBF6glCFgt94U8XP5BT6ppSsDBI8IJ00BU+cAGjQzp6wcKJL2EmRYvxy0TWIg==} + '@tanstack/query-devtools@5.96.2': + resolution: {integrity: sha512-vBTB1Qhbm3nHSbEUtQwks/EdcAtFfEapr1WyBW4w2ExYKuXVi3jIxUIHf5MlSltiHuL7zNyUuanqT/7sI2sb6g==} - '@tanstack/react-devtools@0.10.0': - resolution: {integrity: sha512-cUMzOQb1IHmkb8MsD0TrxHT8EL92Rx3G0Huq+IFkWeoaZPGlIiaIcGTpS5VvQDeI4BVUT+ZGt6CQTpx8oSTECg==} + '@tanstack/react-devtools@0.10.2': + resolution: {integrity: sha512-1BmZyxOrI5SqmRJ5MgkYZNNdnlLsJxQRI2YgorrAvcF2MxK6x5RcuStvD8+YlXoMw3JtNukPxoITirKAnKYDQA==} engines: {node: '>=18'} peerDependencies: '@types/react': '>=16.8' @@ -4014,13 +3986,13 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-form-devtools@0.2.19': - resolution: {integrity: sha512-bILhij/Ye4T1YtyvNctmIShBL0gBp1jnWq0/9KASDFxjXjDUTmFE4TwAzYnwXbARjf6x8ZUW5MuJbi7VjpIGFw==} + '@tanstack/react-form-devtools@0.2.20': + resolution: {integrity: sha512-aXtorJ7p3TbzOapjaxbjGX/c0uQh/wbYSwgzFt3qatNMb1xL4HM/j00Bx7hDENZNBCf8MF8YEEtvpBmnGb4rnQ==} peerDependencies: react: ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/react-form@1.28.5': - resolution: {integrity: sha512-CL8IeWkeXnEEDsHt5wBuIOZvSYrKiLRtsC9ca0LzfJJ22SYCma9cBmh1UX1EBX0o3gH2U21PmUf+y5f9OJNoEQ==} + '@tanstack/react-form@1.28.6': + resolution: {integrity: sha512-dRxwKeNW3uuJvf0sXsIQ2compFMnIJNk9B436Lx0fqkqK+CBvA1tNmEdX+faoCpuQ5Wua3c8ahVibJ65cpkijA==} peerDependencies: '@tanstack/react-start': '*' react: ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -4028,14 +4000,14 @@ packages: '@tanstack/react-start': optional: true - '@tanstack/react-query-devtools@5.95.2': - resolution: {integrity: sha512-AFQFmbznVkbtfpx8VJ2DylW17wWagQel/qLstVLkYmNRo2CmJt3SNej5hvl6EnEeljJIdC3BTB+W7HZtpsH+3g==} + '@tanstack/react-query-devtools@5.96.2': + resolution: {integrity: sha512-nTFKLGuTOFvmFRvcyZ3ArWC/DnMNPoBh6h/2yD6rsf7TCTJCQt+oUWOp2uKPTIuEPtF/vN9Kw5tl5mD1Kbposw==} peerDependencies: - '@tanstack/react-query': ^5.95.2 + '@tanstack/react-query': ^5.96.2 react: ^18 || ^19 - '@tanstack/react-query@5.95.2': - resolution: {integrity: sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA==} + '@tanstack/react-query@5.96.2': + resolution: {integrity: sha512-sYyzzJT4G0g02azzJ8o55VFFV31XvFpdUpG+unxS0vSaYsJnSPKGoI6WdPwUucJL1wpgGfwfmntNX/Ub1uOViA==} peerDependencies: react: ^18 || ^19 @@ -4264,9 +4236,6 @@ packages: '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} - '@types/hast@2.3.10': - resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} - '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -4294,8 +4263,8 @@ packages: '@types/negotiator@0.6.4': resolution: {integrity: sha512-elf6BsTq+AkyNsb2h5cGNst2Mc7dPliVoAPm1fXglC/BM3f2pFA40BaSSv3E5lyHteEawVKLP+8TwiY1DMNb3A==} - '@types/node@25.5.0': - resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} + '@types/node@25.5.2': + resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -4311,12 +4280,6 @@ packages: peerDependencies: '@types/react': ^19.2.0 - '@types/react-syntax-highlighter@15.5.13': - resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==} - - '@types/react-window@1.8.8': - resolution: {integrity: sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==} - '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} @@ -4347,13 +4310,13 @@ packages: '@types/zen-observable@0.8.3': resolution: {integrity: sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw==} - '@typescript-eslint/eslint-plugin@8.57.2': - resolution: {integrity: sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==} + '@typescript-eslint/eslint-plugin@8.58.1': + resolution: {integrity: sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.57.2 + '@typescript-eslint/parser': ^8.58.1 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' '@typescript-eslint/parser@8.57.2': resolution: {integrity: sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==} @@ -4362,12 +4325,25 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/parser@8.58.1': + resolution: {integrity: sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/project-service@8.57.2': resolution: {integrity: sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/project-service@8.58.1': + resolution: {integrity: sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/rule-tester@8.57.2': resolution: {integrity: sha512-cb5m0irr1449waTuYzGi4KD3SGUH3khL4ta/o9lzShvT7gnIwR5qVhU0VM0p966kCrtFId8hwmkvz1fOElsxTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4378,29 +4354,49 @@ packages: resolution: {integrity: sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.58.1': + resolution: {integrity: sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.57.2': resolution: {integrity: sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.57.2': - resolution: {integrity: sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==} + '@typescript-eslint/tsconfig-utils@8.58.1': + resolution: {integrity: sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.58.1': + resolution: {integrity: sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' '@typescript-eslint/types@8.57.2': resolution: {integrity: sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.58.1': + resolution: {integrity: sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.57.2': resolution: {integrity: sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/typescript-estree@8.58.1': + resolution: {integrity: sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@8.57.2': resolution: {integrity: sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4408,47 +4404,58 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/utils@8.58.1': + resolution: {integrity: sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/visitor-keys@8.57.2': resolution: {integrity: sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260329.1': - resolution: {integrity: sha512-zS1thDk7luD82nXVwvMd97F7FgxAE6jGtSmnHeXdaQ+6hJQcQLOVkfUdaehhdodqKDapWA2jEURxQAYjDGvv3g==} + '@typescript-eslint/visitor-keys@8.58.1': + resolution: {integrity: sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260407.1': + resolution: {integrity: sha512-akoBfxvDbULMWLqHPDBI5sRkhjQ0blX5+iG7GBoSstqJZW4P0nzd516COGs7xWHsu3apBhaBgSTMCFO78kG80w==} cpu: [arm64] os: [darwin] - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260329.1': - resolution: {integrity: sha512-3IJ2qmpjQ1OXpZNUhJRjF1+SbDuqGC14Ug8DjWJlPBp06isi1fcJph90f5qW//FxEsNnJPYRcNwpP0A2RbTASg==} + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260407.1': + resolution: {integrity: sha512-j/V5BS+tgcRFGQC+y95vZB78fI45UgobAEY1+NlFZ3Yih9ICKWRfJPcalpiP5vjiO2NgqVzcFfO9XbpJyq5TTA==} cpu: [x64] os: [darwin] - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260329.1': - resolution: {integrity: sha512-gQb6SjB5JlUKDaDuz6mv/m+/OBWVDlcjHINFOykBZZYZtgxBx6nEDjLrT8TiJRjmHEG6hSbv+yisUL9IThWycA==} + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260407.1': + resolution: {integrity: sha512-QG0E0lmcZQZimvNltxyi5Q3Oz1pd0BdztS7K5T9HTs30E3TSeYHq7Csw3SbDfAVwcqs2HTe/AVqLy6ar+1zm3Q==} cpu: [arm64] os: [linux] - '@typescript/native-preview-linux-arm@7.0.0-dev.20260329.1': - resolution: {integrity: sha512-WKSSJrH611DFFAg6YCkgbnkdy0a4RRpzvDpNXtPzLTbMYC5oJdq3Dpvncx5nrJvGh4J4yvzXoMxraGPyygqGLw==} + '@typescript/native-preview-linux-arm@7.0.0-dev.20260407.1': + resolution: {integrity: sha512-ZDr+zQFSTPmLIGyXDWixYFeFtktWUDGAD6s65rTI5EJgyt4X5/kEMnNd04mf4PbN0ChSiTRzJYLzaM+JGo+jww==} cpu: [arm] os: [linux] - '@typescript/native-preview-linux-x64@7.0.0-dev.20260329.1': - resolution: {integrity: sha512-kg4r+ssxoEWruBynUg9bFMdcMpo5NupzAPqNBlV8uWbmYGZjaPLonFWAi9ZZMiVJY/x5ZQ9GBl6xskwLdd3PJQ==} + '@typescript/native-preview-linux-x64@7.0.0-dev.20260407.1': + resolution: {integrity: sha512-a82yGx039yqZBS0dwKG8+kgeF2xVA7Pg6lL2SrswbaxWz3bXpI0ASX3HgUw+JMSIr4fbZ5ulKcaorPqbhc48/A==} cpu: [x64] os: [linux] - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260329.1': - resolution: {integrity: sha512-Qi4lddVxl5MG7Tk67gYhCFnoqqLGd4TvaI8RN4qHFjt3GV8s6c+0cQGsJXJnVgMx27qbyDTdsyAa2pvb42rYcQ==} + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260407.1': + resolution: {integrity: sha512-e38ow5yqBrdiz4GunQCRk1E7cTtowpbXeAvVJf1wXrWbFqEc0D8BE7YPmTy9W2fOI0KFHUrsFg5h4Ad/TKVjug==} cpu: [arm64] os: [win32] - '@typescript/native-preview-win32-x64@7.0.0-dev.20260329.1': - resolution: {integrity: sha512-+k5+usuB8HZ6Xc+enLdb95ZJd25bQqsnI1zXxfRCHP+RS9mxs70Mi9ezQz3lKOLZFFXShSH7iW9iulm8KwVzCQ==} + '@typescript/native-preview-win32-x64@7.0.0-dev.20260407.1': + resolution: {integrity: sha512-1Jiij5NQOvlM72/DdfXzAVia1pdffgHiVgWZVmDwXECpzwQB0WwWfhI/0IddXP92Y9gVQFCGo9lypSAnamfGPA==} cpu: [x64] os: [win32] - '@typescript/native-preview@7.0.0-dev.20260329.1': - resolution: {integrity: sha512-v5lJ0TgSt2m9yVk2xoj9+NH/gTDeWTLaWGPx6MJsUKOYd6bmCJhHbMcWmb8d/zlfhE9ffpixUKYj62CdYfriqA==} + '@typescript/native-preview@7.0.0-dev.20260407.1': + resolution: {integrity: sha512-gf1W3UbzVTDkZJuwhNtOcfQ6l3hpDcxuWh90ANlp/cKupmAqaXNGpT23YjTYqXsaI7RDQR7JUELCKeWbW9PJIg==} hasBin: true '@ungap/structured-clone@1.3.0': @@ -4505,8 +4512,8 @@ packages: babel-plugin-react-compiler: optional: true - '@vitejs/plugin-rsc@0.5.21': - resolution: {integrity: sha512-uNayLT8IKvWoznvQyfwKuGiEFV28o7lxUDnw/Av36VCuGpDFZnMmvVCwR37gTvnSmnpul9V0tdJqY3tBKEaDqw==} + '@vitejs/plugin-rsc@0.5.22': + resolution: {integrity: sha512-OC4wKNVHpF+LOgtasdMOAw1V0yWHj1Nx/XfkNW/9weFXd/9wXPWDyeJGcUJ03DxqJ8mYi4j9/kvo6HKYCoP9Ow==} peerDependencies: react: '*' react-dom: '*' @@ -4516,17 +4523,17 @@ packages: react-server-dom-webpack: optional: true - '@vitest/coverage-v8@4.1.1': - resolution: {integrity: sha512-nZ4RWwGCoGOQRMmU/Q9wlUY540RVRxJZ9lxFsFfy0QV7Zmo5VVBhB6Sl9Xa0KIp2iIs3zWfPlo9LcY1iqbpzCw==} + '@vitest/coverage-v8@4.1.3': + resolution: {integrity: sha512-/MBdrkA8t6hbdCWFKs09dPik774xvs4Z6L4bycdCxYNLHM8oZuRyosumQMG19LUlBsB6GeVpL1q4kFFazvyKGA==} peerDependencies: - '@vitest/browser': 4.1.1 - vitest: 4.1.1 + '@vitest/browser': 4.1.3 + vitest: 4.1.3 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/eslint-plugin@1.6.13': - resolution: {integrity: sha512-ui7JGWBoQpS5NKKW0FDb1eTuFEZ5EupEv2Psemuyfba7DfA5K52SeDLelt6P4pQJJ/4UGkker/BgMk/KrjH3WQ==} + '@vitest/eslint-plugin@1.6.14': + resolution: {integrity: sha512-PXZ5ysw4eHU9h8nDtBvVcGC7Z2C/T9CFdheqSw1NNXFYqViojub0V9bgdYI67iBTOcra2mwD0EYldlY9bGPf2Q==} engines: {node: '>=18'} peerDependencies: '@typescript-eslint/eslint-plugin': '*' @@ -4547,8 +4554,8 @@ packages: '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - '@vitest/pretty-format@4.1.1': - resolution: {integrity: sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ==} + '@vitest/pretty-format@4.1.3': + resolution: {integrity: sha512-hYqqwuMbpkkBodpRh4k4cQSOELxXky1NfMmQvOfKvV8zQHz8x8Dla+2wzElkMkBvSAJX5TRGHJAQvK0TcOafwg==} '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} @@ -4556,19 +4563,19 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@vitest/utils@4.1.1': - resolution: {integrity: sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==} + '@vitest/utils@4.1.3': + resolution: {integrity: sha512-Pc/Oexse/khOWsGB+w3q4yzA4te7W4gpZZAvk+fr8qXfTURZUMj5i7kuxsNK5mP/dEB6ao3jfr0rs17fHhbHdw==} - '@voidzero-dev/vite-plus-core@0.1.14': - resolution: {integrity: sha512-CCWzdkfW0fo0cQNlIsYp5fOuH2IwKuPZEb2UY2Z8gXcp5pG74A82H2Pthj0heAuvYTAnfT7kEC6zM+RbiBgQbg==} + '@voidzero-dev/vite-plus-core@0.1.16': + resolution: {integrity: sha512-fOyf14CXjcXqANFs2fCXEX+0Tn9ZjmqfFV+qTnARwIF1Kzl8WquO4XtvlDgs/fTQ91H4AyoNUgkvWdKS+C4xYA==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@arethetypeswrong/core': ^0.18.1 - '@tsdown/css': 0.21.4 - '@tsdown/exe': 0.21.4 + '@tsdown/css': 0.21.7 + '@tsdown/exe': 0.21.7 '@types/node': ^20.19.0 || >=22.12.0 '@vitejs/devtools': ^0.1.0 - esbuild: 0.27.2 + esbuild: ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 publint: ^0.3.0 @@ -4578,7 +4585,7 @@ packages: sugarss: ^5.0.0 terser: ^5.16.0 tsx: ^4.8.1 - typescript: ^5.0.0 + typescript: ^5.0.0 || ^6.0.0 unplugin-unused: ^0.5.0 yaml: 2.8.3 peerDependenciesMeta: @@ -4619,54 +4626,54 @@ packages: yaml: optional: true - '@voidzero-dev/vite-plus-darwin-arm64@0.1.14': - resolution: {integrity: sha512-q2ESUSbapwsxVRe/KevKATahNRraoX5nti3HT9S3266OHT5sMroBY14jaxTv74ekjQc9E6EPhyLGQWuWQuuBRw==} + '@voidzero-dev/vite-plus-darwin-arm64@0.1.16': + resolution: {integrity: sha512-InG0ZmuGh7DTrn7zWQ0UvKapElphKI6G1oYfys+jraedG70EhIIee9gtO+mTE1T0bF67SgAcLXwNyaiNda0XwA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@voidzero-dev/vite-plus-darwin-x64@0.1.14': - resolution: {integrity: sha512-UpcDZc9G99E/4HDRoobvYHxMvFOG5uv3RwEcq0HF70u4DsnEMl1z8RaJLeWV7a09LGwj9Q+YWC3Z4INWnTLs8g==} + '@voidzero-dev/vite-plus-darwin-x64@0.1.16': + resolution: {integrity: sha512-LGNrECstuhkCRKRj/dE98Xcprw8HU3VMIMJnZsnDR2C5RB2HADNIu21at/a/G3giA9eWm7uhtPp9FvUtTCK9TA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.14': - resolution: {integrity: sha512-GIjn35RABUEDB9gHD26nRq7T72Te+Qy2+NIzogwEaUE728PvPkatF5gMCeF4sigCoc8c4qxDwsG+A2A2LYGnDg==} + '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.16': + resolution: {integrity: sha512-AoFKu6dIOtlkp/mwmtU8ES2uzoaxCHhIym1Tk7qMxyvke4IXnye6VDc4kPMRQwD8mwR3T3bO0HuaEEHxrIWDxw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.14': - resolution: {integrity: sha512-qo2RToGirG0XCcxZ2AEOuonLM256z6dNbJzDDIo5gWYA+cIKigFQJbkPyr25zsT1tsP2aY0OTxt2038XbVlRkQ==} + '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.16': + resolution: {integrity: sha512-PloCsGTRIhcXIpUOJ6PqVG8gYNpq+ooJNyqy5sQ82BRnJuo8oV7uBLFvg0X9B3Bzh+vO1F8/+92+o5TiL35JMg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.14': - resolution: {integrity: sha512-BsMWKZfdfGcYLxxLyaePpg6NW54xqzzcfq8sFUwKfwby0kgOKQ4WymUXyBvO9nnBb0ZPsJQrV0sx+Onac/LTaw==} + '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.16': + resolution: {integrity: sha512-nY9/2g+qjhwsW5U3MrFLlx+bOBsdOJiO2HzbxQy7jo/S3jPTnXhFlrRegQuAmqrHAXrSdNwgblgRpICKhx1xZg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@voidzero-dev/vite-plus-linux-x64-musl@0.1.14': - resolution: {integrity: sha512-mOrEpj7ntW9RopGbcOYG/L0pOs0qHzUG4Vz7NXbuf4dbOSlY4JjyoMOIWxjKQORQht02Hzuf8YrMGNwa6AjVSQ==} + '@voidzero-dev/vite-plus-linux-x64-musl@0.1.16': + resolution: {integrity: sha512-JGKEAMoXqzdr9lHT/13uRNV9uzrSYXAFhjAfIC8WEQMG2VUFksvq5/TOc26hzmzbqu+bxRmfN8h1aVTDL8KwFg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@voidzero-dev/vite-plus-test@0.1.14': - resolution: {integrity: sha512-rjF+qpYD+5+THOJZ3gbE3+cxsk5sW7nJ0ODK7y6ZKeS4amREUMedEDYykzKBwR7OZDC/WwE90A0iLWCr6qAXhA==} + '@voidzero-dev/vite-plus-test@0.1.16': + resolution: {integrity: sha512-d/rJPX/heMzoAFdnpZsp04MAa6nw1yH1tA4mVCV4m8goVcE9nAvt69mjLMzE8N/rYIQOSgenf3hDXuQRuD6OKQ==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/ui': 4.1.1 + '@vitest/ui': 4.1.2 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -4684,14 +4691,14 @@ packages: jsdom: optional: true - '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.14': - resolution: {integrity: sha512-7iC+Ig+8D/zACy0IJf7w/vQ7duTjux9Ttmm3KOBdVWH4dl3JihydA7+SQVMhz71a4WiqJ6nPidoG8D6hUP4MVQ==} + '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.16': + resolution: {integrity: sha512-IugPUCLY7HmiPcCeuHKUqO1+G2vxHnYzAGhS02AixD0sJLTAIKCUANDOiVUFf/HMw+jh/UkugW7MWek8lf/JrQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.14': - resolution: {integrity: sha512-yRJ/8yAYFluNHx0Ej6Kevx65MIeM3wFKklnxosVZRlz2ZRL1Ea1Qh3tWATr3Ipk1ciRxBv8KJgp6zXqjxtZSoQ==} + '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.16': + resolution: {integrity: sha512-tq93CIeMs92HF7rdylJknRiyzMOWMKCmpw+g8nl5Q5nmUDNLUsrL3CGfbyqjgbruuPnIr761r9MfydPqZU/cYg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -4768,6 +4775,9 @@ packages: '@webassemblyjs/wast-printer@1.14.1': resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + '@webcontainer/env@1.1.1': + resolution: {integrity: sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng==} + '@xstate/fsm@1.6.5': resolution: {integrity: sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==} @@ -4940,8 +4950,8 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@1.1.13: + resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} brace-expansion@5.0.5: resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} @@ -4977,20 +4987,10 @@ packages: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} - bundle-require@5.1.0: - resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - peerDependencies: - esbuild: 0.27.2 - bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - cac@7.0.0: resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} engines: {node: '>=20.19.0'} @@ -5033,21 +5033,12 @@ packages: character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} - character-entities-legacy@1.1.4: - resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} - character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} - character-entities@1.2.4: - resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} - character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} - character-reference-invalid@1.1.4: - resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} - character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} @@ -5134,8 +5125,8 @@ packages: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc - code-inspector-plugin@1.4.5: - resolution: {integrity: sha512-yp3zHd5AZhtVoBNOzKQuJVo1wZe7AIO2vAiVhF8WIAK02IwM9+gY+Pr9deajx+XyJLbzMW+3CgdfLIh+xxW2Hg==} + code-inspector-plugin@1.5.1: + resolution: {integrity: sha512-7gOqqBurKCucnls1ZHw0KWb7Z5u7gg3Q2pFSY9rrttFmwRaFJfJiscKEbm7X9IKmeEvkFRtNvNrHbSVQ67L8pQ==} collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} @@ -5147,9 +5138,6 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - comma-separated-tokens@1.0.8: - resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} - comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -5168,10 +5156,6 @@ packages: commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} @@ -5191,16 +5175,15 @@ packages: compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} confbox@0.2.4: resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} - consola@3.4.2: - resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} - engines: {node: ^14.18.0 || >=16.10.0} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -5475,9 +5458,6 @@ packages: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} - defu@6.1.4: - resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} - delaunator@5.1.0: resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} @@ -5485,9 +5465,6 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} - destr@2.0.5: - resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -5662,8 +5639,8 @@ packages: peerDependencies: eslint: ^9.5.0 || ^10.0.0 - eslint-flat-config-utils@3.0.2: - resolution: {integrity: sha512-mPvevWSDQFwgABvyCurwIu6ZdKxGI5NW22/BGDwA1T49NO6bXuxbV9VfJK/tkQoNyPogT6Yu1d57iM0jnZVWmg==} + eslint-flat-config-utils@3.1.0: + resolution: {integrity: sha512-lM+Nwo2CzpuTS/RASQExlEIwk/BQoKqJWX6VbDlLMb/mveqvt9MMrRXFEkG3bseuK6g8noKZLeX82epkILtv4A==} eslint-json-compat-utils@0.2.3: resolution: {integrity: sha512-RbBmDFyu7FqnjE8F0ZxPNzx5UaptdeS9Uu50r7A+D7s/+FCX+ybiyViYEgFUaFIFqSWJgZRTpL5d8Kanxxl2lQ==} @@ -5732,11 +5709,11 @@ packages: peerDependencies: eslint: '*' - eslint-plugin-import-lite@0.5.2: - resolution: {integrity: sha512-XvfdWOC5dSLEI9krIPRlNmKSI2ViIE9pVylzfV9fCq0ZpDaNeUk6o0wZv0OzN83QdadgXp1NsY0qjLINxwYCsw==} + eslint-plugin-import-lite@0.6.0: + resolution: {integrity: sha512-80vevx2A7i3H7n1/6pqDO8cc5wRz6OwLDvIyVl9UflBV1N1f46e9Ihzi65IOLYoSxM6YykK2fTw1xm0Ixx6aTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: '>=9.0.0' + eslint: ^9.0.0 || ^10.0.0 eslint-plugin-jsdoc@62.8.1: resolution: {integrity: sha512-e9358PdHgvcMF98foNd3L7hVCw70Lt+YcSL7JzlJebB8eT5oRJtW6bHMQKoAwJtw6q0q0w/fRIr2kwnHdFDI6A==} @@ -5750,11 +5727,11 @@ packages: peerDependencies: eslint: '>=9.38.0' - eslint-plugin-markdown-preferences@0.40.3: - resolution: {integrity: sha512-R3CCAEFwnnYXukTdtvdsamGjbTgVs9UZKqMKhNeWNXzFtOP1Frc89bgbd56lJUN7ASaxgvzc5fUpKvDCOTtDpg==} + eslint-plugin-markdown-preferences@0.41.0: + resolution: {integrity: sha512-Pu150jKH1Cf5sW/Igck0VbuT0A9qFpIPG1dDvyAt2lG8tA3VzPDkwxBusO8JqQ9NRIrm3pat0X6cfanSki3WZQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@eslint/markdown': ^7.4.0 + '@eslint/markdown': ^7.4.0 || ^8.0.0 eslint: '>=9.0.0' eslint-plugin-n@17.24.0: @@ -5788,12 +5765,6 @@ packages: eslint: ^10.0.0 typescript: '*' - eslint-plugin-react-hooks@7.0.1: - resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} - engines: {node: '>=18'} - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - eslint-plugin-react-naming-convention@3.0.0: resolution: {integrity: sha512-pAtOZST5/NhWIa/I5yz7H1HEZTtCY7LHMhzmN9zvaOdTWyZYtz2g9pxPRDBnkR9uSmHsNt44gj+2JSAD4xwgew==} engines: {node: '>=22.0.0'} @@ -5838,11 +5809,11 @@ packages: peerDependencies: eslint: ^8.0.0 || ^9.0.0 || ^10.0.0 - eslint-plugin-storybook@10.3.3: - resolution: {integrity: sha512-jo8wZvKaJlxxrNvf4hCsROJP3CdlpaLiYewAs5Ww+PJxCrLelIi5XVHWOAgBvvr3H9WDKvUw8xuvqPYqAlpkFg==} + eslint-plugin-storybook@10.3.5: + resolution: {integrity: sha512-rEFkfU3ypF44GpB4tiJ9EFDItueoGvGi3+weLHZax2ON2MB7VIDsxdSUGvIU5tMURg+oWYlpzCyLm4TpDq2deA==} peerDependencies: eslint: '>=8' - storybook: ^10.3.3 + storybook: ^10.3.5 eslint-plugin-toml@1.3.1: resolution: {integrity: sha512-1l00fBP03HIt9IPV7ZxBi7x0y0NMdEZmakL1jBD6N/FoKBvfKxPw5S8XkmzBecOnFBTn5Z8sNJtL5vdf9cpRMQ==} @@ -5850,8 +5821,8 @@ packages: peerDependencies: eslint: '>=9.38.0' - eslint-plugin-unicorn@63.0.0: - resolution: {integrity: sha512-Iqecl9118uQEXYh7adylgEmGfkn5es3/mlQTLLkd4pXkIk9CTGrAbeUux+YljSa2ohXCBmQQ0+Ej1kZaFgcfkA==} + eslint-plugin-unicorn@64.0.0: + resolution: {integrity: sha512-rNZwalHh8i0UfPlhNwg5BTUO1CMdKNmjqe+TgzOTZnpKoi8VBgsW7u9qCHIdpxEzZ1uwrJrPF0uRb7l//K38gA==} engines: {node: ^20.10.0 || >=21.0.0} peerDependencies: eslint: '>=9.38.0' @@ -5915,8 +5886,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.1.0: - resolution: {integrity: sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==} + eslint@10.2.0: + resolution: {integrity: sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -6031,15 +6002,21 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-string-truncated-width@1.2.1: + resolution: {integrity: sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow==} + + fast-string-width@1.1.0: + resolution: {integrity: sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-wrap-ansi@0.1.6: + resolution: {integrity: sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} - fault@1.0.4: - resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==} - fault@2.0.1: resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} @@ -6085,9 +6062,6 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} - fix-dts-default-cjs-exports@1.0.1: - resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} - flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -6131,9 +6105,6 @@ packages: functional-red-black-tree@1.0.1: resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} - fzf@0.5.2: - resolution: {integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==} - gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -6186,10 +6157,6 @@ packages: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} - globals@16.5.0: - resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} - engines: {node: '>=18'} - globals@17.4.0: resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==} engines: {node: '>=18'} @@ -6235,9 +6202,6 @@ packages: hast-util-is-element@3.0.0: resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} - hast-util-parse-selector@2.2.5: - resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==} - hast-util-parse-selector@4.0.0: resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} @@ -6250,6 +6214,9 @@ packages: hast-util-to-estree@3.1.3: resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + hast-util-to-jsx-runtime@2.3.6: resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} @@ -6262,30 +6229,15 @@ packages: hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} - hastscript@6.0.0: - resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} - hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} - hermes-estree@0.25.1: - resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} - - hermes-parser@0.25.1: - resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} - hex-rgb@4.3.0: resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} engines: {node: '>=6'} - highlight.js@10.7.3: - resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} - - highlightjs-vue@1.0.0: - resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==} - - hono@4.12.9: - resolution: {integrity: sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==} + hono@4.12.12: + resolution: {integrity: sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==} engines: {node: '>=16.9.0'} hosted-git-info@9.0.2: @@ -6316,8 +6268,8 @@ packages: i18next-resources-to-backend@1.2.1: resolution: {integrity: sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw==} - i18next@25.10.10: - resolution: {integrity: sha512-cqUW2Z3EkRx7NqSyywjkgCLK7KLCL6IFVFcONG7nVYIJ3ekZ1/N5jUsihHV6Bq37NfhgtczxJcxduELtjTwkuQ==} + i18next@26.0.3: + resolution: {integrity: sha512-1571kXINxHKY7LksWp8wP+zP0YqHSSpl/OW0Y0owFEf2H3s8gCAffWaZivcz14rMkOvn3R/psiQxVsR9t2Nafg==} peerDependencies: typescript: ^5 || ^6 peerDependenciesMeta: @@ -6403,15 +6355,9 @@ packages: resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==} deprecated: The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019. - is-alphabetical@1.0.4: - resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} - is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} - is-alphanumerical@1.0.4: - resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} - is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} @@ -6419,9 +6365,6 @@ packages: resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==} engines: {node: '>=18.20'} - is-decimal@1.0.4: - resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} - is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} @@ -6438,9 +6381,6 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - is-hexadecimal@1.0.4: - resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} - is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} @@ -6469,9 +6409,6 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} - is-reference@3.0.3: - resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} - is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -6506,8 +6443,8 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true - jotai@2.19.0: - resolution: {integrity: sha512-r2wwxEXP1F2JteDLZEOPoIpAHhV89paKsN5GWVYndPNMMP/uVZDcC+fNj0A8NjKgaPWzdyO8Vp8YcYKe0uCEqQ==} + jotai@2.19.1: + resolution: {integrity: sha512-sqm9lVZiqBHZH8aSRk32DSiZDHY3yUIlulXYn9GQj7/LvoUdYXSMti7ZPJGo+6zjzKFt5a25k/I6iBCi43PJcw==} engines: {node: '>=12.20.0'} peerDependencies: '@babel/core': '>=7.0.0' @@ -6524,10 +6461,6 @@ packages: react: optional: true - joycon@3.1.1: - resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} - engines: {node: '>=10'} - js-audio-recorder@1.0.7: resolution: {integrity: sha512-JiDODCElVHGrFyjGYwYyNi7zCbKk9va9C77w+zCPMmi4C6ix7zsX2h3ddHugmo4dOTOTCym9++b/wVW9nC0IaA==} @@ -6594,8 +6527,8 @@ packages: resolution: {integrity: sha512-eQQBjBnsVtGacsG9uJNB8qOr3yA8rga4wAaGG1qRcBzSIvfhERLrWxMAM1hp5fcS6Abo8M4+bUBTekYR0qTPQw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - katex@0.16.44: - resolution: {integrity: sha512-EkxoDTk8ufHqHlf9QxGwcxeLkWRR3iOuYfRpfORgYfqc8s13bgb+YtRY59NK5ZpRaCwq1kqA6a5lpX8C/eLphQ==} + katex@0.16.45: + resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==} hasBin: true keyv@4.5.4: @@ -6604,8 +6537,8 @@ packages: khroma@2.1.0: resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} - knip@6.1.0: - resolution: {integrity: sha512-n5eVbJP7HXmwTsiJcELWJe2O1ESxyCTNxJzRTIECDYDTM465qnqk7fL2dv6ae3NUFvFWorZvGlh9mcwxwJ5Xgw==} + knip@6.3.0: + resolution: {integrity: sha512-g6dVPoTw6iNm3cubC5IWxVkVsd0r5hXhTBTbAGIEQN53GdA2ZM/slMTPJ7n5l8pBebNQPHpxjmKxuR4xVQ2/hQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -6615,9 +6548,9 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} - ky@1.14.3: - resolution: {integrity: sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw==} - engines: {node: '>=18'} + ky@2.0.0: + resolution: {integrity: sha512-KzI4Vz5AbZFAUFYGx28PCSfFWUo6/qj9Br/P6KRwDieE1xfdz0tIONepJcLw/1xLocN13GgvfJGasa+pfSkbHg==} + engines: {node: '>=22'} lamejs@1.2.1: resolution: {integrity: sha512-s7bxvjvYthw6oPLCm5pFxvA84wUROODB8jEO2+CE1adhKgrIvVOlmMgY8zyugxGrvRaDHNJanOiS21/emty6dQ==} @@ -6727,20 +6660,9 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} - lilconfig@3.1.3: - resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} - engines: {node: '>=14'} - linebreak@1.1.0: resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - load-tsconfig@0.2.5: - resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - loader-runner@4.3.1: resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} engines: {node: '>=6.11.5'} @@ -6753,8 +6675,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash-es@4.17.23: - resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + lodash-es@4.18.0: + resolution: {integrity: sha512-koAgswPPA+UTaPN64Etp+PGP+WT6oqOS2NMi5yDkMaiGw9qY4VxQbQF0mtKMyr4BlTznWyzePV5UpECTJQmSUA==} + deprecated: Bad release. Please use lodash-es@4.17.23 instead. lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -6765,8 +6688,9 @@ packages: lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} - lodash@4.17.23: - resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + lodash@4.18.0: + resolution: {integrity: sha512-l1mfj2atMqndAHI3ls7XqPxEjV2J9ZkcNyHpoZA3r2T1LLwDB69jgkMWh71YKwhBbK0G2f4WSn05ahmQXVxupA==} + deprecated: Bad release. Please use lodash@4.17.21 instead. longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -6781,9 +6705,6 @@ packages: lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} - lowlight@1.20.0: - resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} - lru-cache@11.2.7: resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} engines: {node: 20 || >=22} @@ -6900,9 +6821,6 @@ packages: mdn-data@2.23.0: resolution: {integrity: sha512-786vq1+4079JSeu2XdcDjrhi/Ry7BWtjDl9WtGPWLiIHb2T66GvIVflZTBoSNZ5JqTtJGYEVMuFA/lbQlMOyDQ==} - memoize-one@5.2.1: - resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} - merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -6910,8 +6828,8 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - mermaid@11.13.0: - resolution: {integrity: sha512-fEnci+Immw6lKMFI8sqzjlATTyjLkRa6axrEgLV2yHTfv8r+h1wjFbV6xeRtd4rUV1cS4EpR9rwp3Rci7TRWDw==} + mermaid@11.14.0: + resolution: {integrity: sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==} micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -7049,10 +6967,6 @@ packages: engines: {node: '>=16'} hasBin: true - mimic-function@5.0.1: - resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} - engines: {node: '>=18'} - mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -7143,8 +7057,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@16.2.1: - resolution: {integrity: sha512-VaChzNL7o9rbfdt60HUj8tev4m6d7iC1igAy157526+cJlXOQu5LzsBXNT+xaJnTP/k+utSX5vMv7m0G+zKH+Q==} + next@16.2.2: + resolution: {integrity: sha512-i6AJdyVa4oQjyvX/6GeER8dpY/xlIV+4NMv/svykcLtURJSy/WzDnnUk/TM4d0uewFHK7xSQz4TbIwPgjky+3A==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -7174,9 +7088,6 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - node-fetch-native@1.6.7: - resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} - node-releases@2.0.36: resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} @@ -7221,18 +7132,17 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - ofetch@1.5.1: - resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} - ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - onetime@7.0.0: - resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} - engines: {node: '>=18'} + oniguruma-parser@0.12.1: + resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} + + oniguruma-to-es@4.3.5: + resolution: {integrity: sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==} open@10.2.0: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} @@ -7256,21 +7166,21 @@ packages: oxc-resolver@11.19.1: resolution: {integrity: sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==} - oxfmt@0.42.0: - resolution: {integrity: sha512-QhejGErLSMReNuZ6vxgFHDyGoPbjTRNi6uGHjy0cvIjOQFqD6xmr/T+3L41ixR3NIgzcNiJ6ylQKpvShTgDfqg==} + oxfmt@0.43.0: + resolution: {integrity: sha512-KTYNG5ISfHSdmeZ25Xzb3qgz9EmQvkaGAxgBY/p38+ZiAet3uZeu7FnMwcSQJg152Qwl0wnYAxDc+Z/H6cvrwA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - oxlint-tsgolint@0.17.3: - resolution: {integrity: sha512-1eh4bcpOMw0e7+YYVxmhFc2mo/V6hJ2+zfukqf+GprvVn3y94b69M/xNrYLmx5A+VdYe0i/bJ2xOs6Hp/jRmRA==} + oxlint-tsgolint@0.20.0: + resolution: {integrity: sha512-/Uc9TQyN1l8w9QNvXtVHYtz+SzDJHKpb5X0UnHodl0BVzijUPk0LPlDOHAvogd1UI+iy9ZSF6gQxEqfzUxCULQ==} hasBin: true - oxlint@1.57.0: - resolution: {integrity: sha512-DGFsuBX5MFZX9yiDdtKjTrYPq45CZ8Fft6qCltJITYZxfwYjVdGf/6wycGYTACloauwIPxUnYhBVeZbHvleGhw==} + oxlint@1.58.0: + resolution: {integrity: sha512-t4s9leczDMqlvOSjnbCQe7gtoLkWgBGZ7sBdCJ9EOj5IXFSG/X7OAzK4yuH4iW+4cAYe8kLFbC8tuYMwWZm+Cg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - oxlint-tsgolint: '>=0.15.0' + oxlint-tsgolint: '>=0.18.0' peerDependenciesMeta: oxlint-tsgolint: optional: true @@ -7307,9 +7217,6 @@ packages: parse-css-color@0.2.1: resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==} - parse-entities@2.0.0: - resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} - parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} @@ -7381,9 +7288,6 @@ packages: perfect-debounce@2.1.0: resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} - periscopic@4.0.2: - resolution: {integrity: sha512-sqpQDUy8vgB7ycLkendSKS6HnVz1Rneoc3Rc+ZBUCe2pbqlVuCC5vF52l0NJ1aiMg/r1qfYF9/myz8CZeI2rjA==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -7398,10 +7302,6 @@ packages: pinyin-pro@3.28.0: resolution: {integrity: sha512-mMRty6RisoyYNphJrTo3pnvp3w8OMZBrXm9YSWkxhAfxKj1KZk2y8T2PDIZlDDRsvZ0No+Hz6FI4sZpA6Ey25g==} - pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} - engines: {node: '>= 6'} - pixelmatch@7.1.0: resolution: {integrity: sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==} hasBin: true @@ -7412,13 +7312,13 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} - playwright-core@1.58.2: - resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} engines: {node: '>=18'} hasBin: true - playwright@1.58.2: - resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + playwright@1.59.1: + resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} engines: {node: '>=18'} hasBin: true @@ -7443,24 +7343,6 @@ packages: resolution: {integrity: sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==} engines: {node: '>= 10.12'} - postcss-load-config@6.0.1: - resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} - engines: {node: '>= 18'} - peerDependencies: - jiti: '>=1.21.0' - postcss: '>=8.0.9' - tsx: ^4.8.1 - yaml: 2.8.3 - peerDependenciesMeta: - jiti: - optional: true - postcss: - optional: true - tsx: - optional: true - yaml: - optional: true - postcss-selector-parser@6.0.10: resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} engines: {node: '>=4'} @@ -7476,8 +7358,8 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + postcss@8.5.9: + resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} engines: {node: ^10 || ^12 || >=14} powershell-utils@0.1.0: @@ -7498,10 +7380,6 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - prismjs@1.30.0: - resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} - engines: {node: '>=6'} - progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -7512,9 +7390,6 @@ packages: property-expr@2.0.6: resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} - property-information@5.6.0: - resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} - property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} @@ -7537,9 +7412,6 @@ packages: quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} - quansync@1.0.0: - resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} - queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -7602,10 +7474,10 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' - react-i18next@16.6.6: - resolution: {integrity: sha512-ZgL2HUoW34UKUkOV7uSQFE1CDnRPD+tCR3ywSuWH7u2iapnz86U8Bi3Vrs620qNDzCf1F47NxglCEkchCTDOHw==} + react-i18next@17.0.2: + resolution: {integrity: sha512-shBftH2vaTWK2Bsp7FiL+cevx3xFJlvFxmsDFQSrJc+6twHkP0tv/bGa01VVWzpreUVVwU+3Hev5iFqRg65RwA==} peerDependencies: - i18next: '>= 25.10.9' + i18next: '>= 26.0.1' react: '>= 16.8.0' react-dom: '*' react-native: '*' @@ -7692,24 +7564,12 @@ packages: '@types/react': optional: true - react-syntax-highlighter@15.6.6: - resolution: {integrity: sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==} - peerDependencies: - react: '>= 0.14.0' - react-textarea-autosize@8.5.9: resolution: {integrity: sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==} engines: {node: '>=10'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-window@1.8.11: - resolution: {integrity: sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==} - engines: {node: '>8.0.0'} - peerDependencies: - react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react@19.2.4: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} @@ -7765,8 +7625,14 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} - refractor@3.6.0: - resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==} + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} regexp-ast-analysis@0.7.1: resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} @@ -7847,10 +7713,6 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -7859,10 +7721,6 @@ packages: engines: {node: '>= 0.4'} hasBin: true - restore-cursor@5.1.0: - resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} - engines: {node: '>=18'} - reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -7883,9 +7741,6 @@ packages: roughjs@4.6.6: resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} - rsc-html-stream@0.0.7: - resolution: {integrity: sha512-v9+fuY7usTgvXdNl8JmfXCvSsQbq2YMd60kOeeMIqCJFZ69fViuIxztHei7v5mlMMa2h3SqS+v44Gu9i9xANZA==} - run-applescript@7.1.0: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} @@ -7967,9 +7822,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} + shiki@4.0.2: + resolution: {integrity: sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==} + engines: {node: '>=20'} simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} @@ -8012,9 +7867,6 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} - space-separated-tokens@1.1.5: - resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} - space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -8033,8 +7885,8 @@ packages: spdx-license-ids@3.0.23: resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} - srvx@0.11.13: - resolution: {integrity: sha512-oknN6qduuMPafxKtHucUeG32Q963pjriA5g3/Bl05cwEsUe5VVbIU4qR9LrALHbipSCyBe+VmfDGGydqazDRkw==} + srvx@0.11.15: + resolution: {integrity: sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg==} engines: {node: '>=20.16.0'} hasBin: true @@ -8051,8 +7903,8 @@ packages: resolution: {integrity: sha512-9SN0XIjBBXCT6ZXXVnScJN4KP2RyFg6B8sEoFlugVHMANysfaEni4LTWlvUQQ/R0wgZl1Ovt9KBQbzn21kHoZA==} engines: {node: '>=20.19.0'} - storybook@10.3.3: - resolution: {integrity: sha512-tMoRAts9EVqf+mEMPLC6z1DPyHbcPe+CV1MhLN55IKsl0HxNjvVGK44rVPSePbltPE6vIsn4bdRj6CCUt8SJwQ==} + storybook@10.3.5: + resolution: {integrity: sha512-uBSZu/GZa9aEIW3QMGvdQPMZWhGxSe4dyRWU8B3/Vd47Gy/XLC7tsBxRr13txmmPOEDHZR94uLuq0H50fvuqBw==} hasBin: true peerDependencies: prettier: ^2 || ^3 @@ -8142,11 +7994,6 @@ packages: stylis@4.3.6: resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} - sucrase@3.35.1: - resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -8205,10 +8052,6 @@ packages: resolution: {integrity: sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==} engines: {node: '>=18'} - taze@19.10.0: - resolution: {integrity: sha512-pylMr+Yl8m4ZXu5LwWdtfCOJhLW69NuoeZTLtRzTekfheQ1ix5wOWjQlTb8S3SSxLlDcYFuajQOWllO5iyE0jg==} - hasBin: true - terser-webpack-plugin@5.4.0: resolution: {integrity: sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==} engines: {node: '>= 10.13.0'} @@ -8252,9 +8095,6 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyexec@1.0.4: resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} engines: {node: '>=18'} @@ -8279,11 +8119,11 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} - tldts-core@7.0.27: - resolution: {integrity: sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==} + tldts-core@7.0.28: + resolution: {integrity: sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==} - tldts@7.0.27: - resolution: {integrity: sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==} + tldts@7.0.28: + resolution: {integrity: sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==} hasBin: true to-regex-range@5.0.1: @@ -8308,10 +8148,6 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true - trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -8336,9 +8172,6 @@ packages: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} - ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - ts-pattern@5.9.0: resolution: {integrity: sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==} @@ -8369,25 +8202,6 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsup@8.5.1: - resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - '@microsoft/api-extractor': ^7.36.0 - '@swc/core': ^1 - postcss: ^8.4.12 - typescript: '>=4.5.0' - peerDependenciesMeta: - '@microsoft/api-extractor': - optional: true - '@swc/core': - optional: true - postcss: - optional: true - typescript: - optional: true - tsx@4.21.0: resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} @@ -8415,8 +8229,8 @@ packages: resolution: {integrity: sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==} engines: {node: '>=20'} - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + typescript@6.0.2: + resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} engines: {node: '>=14.17'} hasBin: true @@ -8432,12 +8246,6 @@ packages: resolution: {integrity: sha512-X2wH19RAPZE3+ldGicOkoj/SIA83OIxcJ6Cuaw23hf8Xc6fQpvZXY0SftE2JgS0QhYLUG4uwodSI3R53keyh7w==} engines: {node: '>=14'} - unconfig-core@7.5.0: - resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} - - unconfig@7.5.0: - resolution: {integrity: sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==} - undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} @@ -8601,8 +8409,8 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vinext@0.0.38: - resolution: {integrity: sha512-zlQswirXCApDgAFq1eoO/YbRlavGE+Bnowz5vXoQa2EmbFhYg52+T8SZs1QWdOqkbZMhpLIV/iaWvHtkRv2t4Q==} + vinext@0.0.40: + resolution: {integrity: sha512-rs0z6G2el6kS/667ERKQjSMF3R8ZD2H9xDrnRntVOa6OBnyYcOMM/AVpOy/W1lxOkq6EYTO1OUD9DbNSWxRRJw==} engines: {node: '>=22'} hasBin: true peerDependencies: @@ -8644,8 +8452,8 @@ packages: storybook: ^0.0.0-0 || ^9.0.0 || ^10.0.0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - vite-plus@0.1.14: - resolution: {integrity: sha512-p4pWlpZZNiEsHxPWNdeIU9iuPix3ydm3ficb0dXPggoyIkdotfXtvn2NPX9KwfiQImU72EVEs4+VYBZYNcUYrw==} + vite-plus@0.1.16: + resolution: {integrity: sha512-sgYHc5zWLSDInaHb/abvEA7UOwh7sUWuyNt+Slphj55jPvzodT8Dqw115xyKwDARTuRFSpm1eo/t58qZ8/NylQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -8705,10 +8513,10 @@ packages: yaml: optional: true - vitefu@1.1.2: - resolution: {integrity: sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==} + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: vite: optional: true @@ -8832,10 +8640,6 @@ packages: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} - xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -8877,15 +8681,6 @@ packages: zen-observable@0.10.0: resolution: {integrity: sha512-iI3lT0iojZhKwT5DaFy2Ce42n3yFcLdFyOh01G7H0flMY60P8MJuVFEoJoNwXlmAyQ45GrjL6AcZmmlv8A5rbw==} - zimmerframe@1.1.4: - resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} - - zod-validation-error@4.0.2: - resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} - engines: {node: '>=18.0.0'} - peerDependencies: - zod: ^3.25.0 || ^4.0.0 - zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -8939,27 +8734,27 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@amplitude/analytics-browser@2.38.0': + '@amplitude/analytics-browser@2.38.1': dependencies: - '@amplitude/analytics-core': 2.44.0 - '@amplitude/plugin-autocapture-browser': 1.25.0 - '@amplitude/plugin-custom-enrichment-browser': 0.1.2 - '@amplitude/plugin-network-capture-browser': 1.9.11 - '@amplitude/plugin-page-url-enrichment-browser': 0.7.3 - '@amplitude/plugin-page-view-tracking-browser': 2.9.4 - '@amplitude/plugin-web-vitals-browser': 1.1.26 + '@amplitude/analytics-core': 2.44.1 + '@amplitude/plugin-autocapture-browser': 1.25.1 + '@amplitude/plugin-custom-enrichment-browser': 0.1.3 + '@amplitude/plugin-network-capture-browser': 1.9.12 + '@amplitude/plugin-page-url-enrichment-browser': 0.7.4 + '@amplitude/plugin-page-view-tracking-browser': 2.9.5 + '@amplitude/plugin-web-vitals-browser': 1.1.27 tslib: 2.8.1 - '@amplitude/analytics-client-common@2.4.41': + '@amplitude/analytics-client-common@2.4.42': dependencies: '@amplitude/analytics-connector': 1.6.4 - '@amplitude/analytics-core': 2.44.0 + '@amplitude/analytics-core': 2.44.1 '@amplitude/analytics-types': 2.11.1 tslib: 2.8.1 '@amplitude/analytics-connector@1.6.4': {} - '@amplitude/analytics-core@2.44.0': + '@amplitude/analytics-core@2.44.1': dependencies: '@amplitude/analytics-connector': 1.6.4 '@types/zen-observable': 0.8.3 @@ -8973,48 +8768,48 @@ snapshots: dependencies: js-base64: 3.7.8 - '@amplitude/plugin-autocapture-browser@1.25.0': + '@amplitude/plugin-autocapture-browser@1.25.1': dependencies: - '@amplitude/analytics-core': 2.44.0 + '@amplitude/analytics-core': 2.44.1 tslib: 2.8.1 - '@amplitude/plugin-custom-enrichment-browser@0.1.2': + '@amplitude/plugin-custom-enrichment-browser@0.1.3': dependencies: - '@amplitude/analytics-core': 2.44.0 + '@amplitude/analytics-core': 2.44.1 tslib: 2.8.1 - '@amplitude/plugin-network-capture-browser@1.9.11': + '@amplitude/plugin-network-capture-browser@1.9.12': dependencies: - '@amplitude/analytics-core': 2.44.0 + '@amplitude/analytics-core': 2.44.1 tslib: 2.8.1 - '@amplitude/plugin-page-url-enrichment-browser@0.7.3': + '@amplitude/plugin-page-url-enrichment-browser@0.7.4': dependencies: - '@amplitude/analytics-core': 2.44.0 + '@amplitude/analytics-core': 2.44.1 tslib: 2.8.1 - '@amplitude/plugin-page-view-tracking-browser@2.9.4': + '@amplitude/plugin-page-view-tracking-browser@2.9.5': dependencies: - '@amplitude/analytics-core': 2.44.0 + '@amplitude/analytics-core': 2.44.1 tslib: 2.8.1 - '@amplitude/plugin-session-replay-browser@1.27.5(@amplitude/rrweb@2.0.0-alpha.37)(rollup@4.59.0)': + '@amplitude/plugin-session-replay-browser@1.27.6(@amplitude/rrweb@2.0.0-alpha.37)(rollup@4.59.0)': dependencies: - '@amplitude/analytics-client-common': 2.4.41 - '@amplitude/analytics-core': 2.44.0 + '@amplitude/analytics-client-common': 2.4.42 + '@amplitude/analytics-core': 2.44.1 '@amplitude/analytics-types': 2.11.1 '@amplitude/rrweb-plugin-console-record': 2.0.0-alpha.36(@amplitude/rrweb@2.0.0-alpha.37) '@amplitude/rrweb-record': 2.0.0-alpha.36 - '@amplitude/session-replay-browser': 1.35.0(@amplitude/rrweb@2.0.0-alpha.37)(rollup@4.59.0) + '@amplitude/session-replay-browser': 1.35.1(@amplitude/rrweb@2.0.0-alpha.37)(rollup@4.59.0) idb-keyval: 6.2.2 tslib: 2.8.1 transitivePeerDependencies: - '@amplitude/rrweb' - rollup - '@amplitude/plugin-web-vitals-browser@1.1.26': + '@amplitude/plugin-web-vitals-browser@1.1.27': dependencies: - '@amplitude/analytics-core': 2.44.0 + '@amplitude/analytics-core': 2.44.1 tslib: 2.8.1 web-vitals: 5.1.0 @@ -9038,7 +8833,7 @@ snapshots: '@amplitude/rrweb-snapshot@2.0.0-alpha.37': dependencies: - postcss: 8.5.8 + postcss: 8.5.9 '@amplitude/rrweb-types@2.0.0-alpha.36': {} @@ -9059,10 +8854,10 @@ snapshots: base64-arraybuffer: 1.0.2 mitt: 3.0.1 - '@amplitude/session-replay-browser@1.35.0(@amplitude/rrweb@2.0.0-alpha.37)(rollup@4.59.0)': + '@amplitude/session-replay-browser@1.35.1(@amplitude/rrweb@2.0.0-alpha.37)(rollup@4.59.0)': dependencies: - '@amplitude/analytics-client-common': 2.4.41 - '@amplitude/analytics-core': 2.44.0 + '@amplitude/analytics-client-common': 2.4.42 + '@amplitude/analytics-core': 2.44.1 '@amplitude/analytics-types': 2.11.1 '@amplitude/experiment-core': 0.7.2 '@amplitude/rrweb-packer': 2.0.0-alpha.36 @@ -9080,57 +8875,56 @@ snapshots: '@amplitude/targeting@0.2.0': dependencies: - '@amplitude/analytics-client-common': 2.4.41 - '@amplitude/analytics-core': 2.44.0 + '@amplitude/analytics-client-common': 2.4.42 + '@amplitude/analytics-core': 2.44.1 '@amplitude/analytics-types': 2.11.1 '@amplitude/experiment-core': 0.7.2 idb: 8.0.0 tslib: 2.8.1 - '@antfu/eslint-config@7.7.3(@eslint-react/eslint-plugin@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(@next/eslint-plugin-next@16.2.1)(@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3))(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(@vue/compiler-sfc@3.5.31)(eslint-plugin-react-hooks@7.0.1(eslint@10.1.0(jiti@2.6.1)))(eslint-plugin-react-refresh@0.5.2(eslint@10.1.0(jiti@2.6.1)))(eslint@10.1.0(jiti@2.6.1))(oxlint@1.57.0(oxlint-tsgolint@0.17.3))(typescript@5.9.3)': + '@antfu/eslint-config@8.0.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@next/eslint-plugin-next@16.2.2)(@typescript-eslint/rule-tester@8.57.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@typescript-eslint/typescript-estree@8.58.1(typescript@6.0.2))(@typescript-eslint/utils@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@vue/compiler-sfc@3.5.31)(eslint-plugin-react-refresh@0.5.2(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(oxlint@1.58.0(oxlint-tsgolint@0.20.0))(typescript@6.0.2)': dependencies: '@antfu/install-pkg': 1.1.0 - '@clack/prompts': 1.1.0 - '@e18e/eslint-plugin': 0.2.0(eslint@10.1.0(jiti@2.6.1))(oxlint@1.57.0(oxlint-tsgolint@0.17.3)) - '@eslint-community/eslint-plugin-eslint-comments': 4.7.1(eslint@10.1.0(jiti@2.6.1)) - '@eslint/markdown': 7.5.1 - '@stylistic/eslint-plugin': 5.10.0(eslint@10.1.0(jiti@2.6.1)) - '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@vitest/eslint-plugin': 1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@clack/prompts': 1.2.0 + '@e18e/eslint-plugin': 0.3.0(eslint@10.2.0(jiti@2.6.1))(oxlint@1.58.0(oxlint-tsgolint@0.20.0)) + '@eslint-community/eslint-plugin-eslint-comments': 4.7.1(eslint@10.2.0(jiti@2.6.1)) + '@eslint/markdown': 8.0.1 + '@stylistic/eslint-plugin': 5.10.0(eslint@10.2.0(jiti@2.6.1)) + '@typescript-eslint/eslint-plugin': 8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/parser': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@vitest/eslint-plugin': 1.6.14(@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) ansis: 4.2.0 cac: 7.0.0 - eslint: 10.1.0(jiti@2.6.1) - eslint-config-flat-gitignore: 2.3.0(eslint@10.1.0(jiti@2.6.1)) - eslint-flat-config-utils: 3.0.2 - eslint-merge-processors: 2.0.0(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-antfu: 3.2.2(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-command: 3.5.2(@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3))(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-import-lite: 0.5.2(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-jsdoc: 62.8.1(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-jsonc: 3.1.2(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-n: 17.24.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.2.0(jiti@2.6.1) + eslint-config-flat-gitignore: 2.3.0(eslint@10.2.0(jiti@2.6.1)) + eslint-flat-config-utils: 3.1.0 + eslint-merge-processors: 2.0.0(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-antfu: 3.2.2(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-command: 3.5.2(@typescript-eslint/rule-tester@8.57.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@typescript-eslint/typescript-estree@8.58.1(typescript@6.0.2))(@typescript-eslint/utils@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-import-lite: 0.6.0(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-jsdoc: 62.8.1(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-jsonc: 3.1.2(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-n: 17.24.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) eslint-plugin-no-only-tests: 3.3.0 - eslint-plugin-perfectionist: 5.7.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-pnpm: 1.6.0(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-regexp: 3.1.0(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-toml: 1.3.1(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-unicorn: 63.0.0(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-vue: 10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.1.0(jiti@2.6.1)))(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@2.6.1))) - eslint-plugin-yml: 3.3.1(eslint@10.1.0(jiti@2.6.1)) - eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.31)(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-perfectionist: 5.7.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + eslint-plugin-pnpm: 1.6.0(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-regexp: 3.1.0(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-toml: 1.3.1(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-unicorn: 64.0.0(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-vue: 10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.2.0(jiti@2.6.1)))(@typescript-eslint/parser@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.2.0(jiti@2.6.1))) + eslint-plugin-yml: 3.3.1(eslint@10.2.0(jiti@2.6.1)) + eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.31)(eslint@10.2.0(jiti@2.6.1)) globals: 17.4.0 local-pkg: 1.1.2 parse-gitignore: 2.0.0 toml-eslint-parser: 1.0.3 - vue-eslint-parser: 10.4.0(eslint@10.1.0(jiti@2.6.1)) + vue-eslint-parser: 10.4.0(eslint@10.2.0(jiti@2.6.1)) yaml-eslint-parser: 2.0.0 optionalDependencies: - '@eslint-react/eslint-plugin': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@next/eslint-plugin-next': 16.2.1 - eslint-plugin-react-hooks: 7.0.1(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-react-refresh: 0.5.2(eslint@10.1.0(jiti@2.6.1)) + '@eslint-react/eslint-plugin': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@next/eslint-plugin-next': 16.2.2 + eslint-plugin-react-refresh: 0.5.2(eslint@10.2.0(jiti@2.6.1)) transitivePeerDependencies: - '@eslint/json' - '@typescript-eslint/rule-tester' @@ -9147,14 +8941,6 @@ snapshots: package-manager-detector: 1.6.0 tinyexec: 1.0.4 - '@antfu/ni@28.3.0': - dependencies: - ansis: 4.2.0 - fzf: 0.5.2 - package-manager-detector: 1.6.0 - tinyexec: 1.0.4 - tinyglobby: 0.2.15 - '@antfu/utils@8.1.1': {} '@babel/code-frame@7.29.0': @@ -9291,12 +9077,12 @@ snapshots: dependencies: '@chevrotain/gast': 11.1.2 '@chevrotain/types': 11.1.2 - lodash-es: 4.17.23 + lodash-es: 4.18.0 '@chevrotain/gast@11.1.2': dependencies: '@chevrotain/types': 11.1.2 - lodash-es: 4.17.23 + lodash-es: 4.18.0 '@chevrotain/regexp-to-ast@11.1.2': {} @@ -9304,13 +9090,13 @@ snapshots: '@chevrotain/utils@11.1.2': {} - '@chromatic-com/storybook@5.1.1(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + '@chromatic-com/storybook@5.1.1(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': dependencies: '@neoconfetti/react': 1.0.0 chromatic: 13.3.5 filesize: 10.1.6 jsonfile: 6.2.0 - storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) strip-ansi: 7.2.0 transitivePeerDependencies: - '@chromatic-com/cypress' @@ -9321,8 +9107,9 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 - '@clack/core@1.1.0': + '@clack/core@1.2.0': dependencies: + fast-wrap-ansi: 0.1.6 sisteransi: 1.0.5 '@clack/prompts@0.8.2': @@ -9331,12 +9118,14 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 - '@clack/prompts@1.1.0': + '@clack/prompts@1.2.0': dependencies: - '@clack/core': 1.1.0 + '@clack/core': 1.2.0 + fast-string-width: 1.1.0 + fast-wrap-ansi: 0.1.6 sisteransi: 1.0.5 - '@code-inspector/core@1.4.5': + '@code-inspector/core@1.5.1': dependencies: '@vue/compiler-dom': 3.5.31 chalk: 4.1.2 @@ -9346,35 +9135,35 @@ snapshots: transitivePeerDependencies: - supports-color - '@code-inspector/esbuild@1.4.5': + '@code-inspector/esbuild@1.5.1': dependencies: - '@code-inspector/core': 1.4.5 + '@code-inspector/core': 1.5.1 transitivePeerDependencies: - supports-color - '@code-inspector/mako@1.4.5': + '@code-inspector/mako@1.5.1': dependencies: - '@code-inspector/core': 1.4.5 + '@code-inspector/core': 1.5.1 transitivePeerDependencies: - supports-color - '@code-inspector/turbopack@1.4.5': + '@code-inspector/turbopack@1.5.1': dependencies: - '@code-inspector/core': 1.4.5 - '@code-inspector/webpack': 1.4.5 + '@code-inspector/core': 1.5.1 + '@code-inspector/webpack': 1.5.1 transitivePeerDependencies: - supports-color - '@code-inspector/vite@1.4.5': + '@code-inspector/vite@1.5.1': dependencies: - '@code-inspector/core': 1.4.5 + '@code-inspector/core': 1.5.1 chalk: 4.1.1 transitivePeerDependencies: - supports-color - '@code-inspector/webpack@1.4.5': + '@code-inspector/webpack@1.5.1': dependencies: - '@code-inspector/core': 1.4.5 + '@code-inspector/core': 1.5.1 transitivePeerDependencies: - supports-color @@ -9488,12 +9277,12 @@ snapshots: '@cucumber/tag-expressions@9.1.0': {} - '@e18e/eslint-plugin@0.2.0(eslint@10.1.0(jiti@2.6.1))(oxlint@1.57.0(oxlint-tsgolint@0.17.3))': + '@e18e/eslint-plugin@0.3.0(eslint@10.2.0(jiti@2.6.1))(oxlint@1.58.0(oxlint-tsgolint@0.20.0))': dependencies: - eslint-plugin-depend: 1.5.0(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-depend: 1.5.0(eslint@10.2.0(jiti@2.6.1)) optionalDependencies: - eslint: 10.1.0(jiti@2.6.1) - oxlint: 1.57.0(oxlint-tsgolint@0.17.3) + eslint: 10.2.0(jiti@2.6.1) + oxlint: 1.58.0(oxlint-tsgolint@0.20.0) '@egoist/tailwindcss-icons@1.9.2(tailwindcss@4.2.2)': dependencies: @@ -9521,7 +9310,7 @@ snapshots: '@es-joy/jsdoccomment@0.84.0': dependencies: '@types/estree': 1.0.8 - '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/types': 8.58.1 comment-parser: 1.4.5 esquery: 1.7.0 jsdoc-type-pratt-parser: 7.1.1 @@ -9606,15 +9395,15 @@ snapshots: '@esbuild/win32-x64@0.27.2': optional: true - '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@10.1.0(jiti@2.6.1))': + '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@10.2.0(jiti@2.6.1))': dependencies: escape-string-regexp: 4.0.0 - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) ignore: 7.0.5 - '@eslint-community/eslint-utils@4.9.1(eslint@10.1.0(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.2.0(jiti@2.6.1))': dependencies: - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/eslint-utils@4.9.1(eslint@9.27.0(jiti@2.6.1))': @@ -9624,77 +9413,77 @@ snapshots: '@eslint-community/regexpp@4.12.2': {} - '@eslint-react/ast@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/ast@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.1.0(jiti@2.6.1) + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + eslint: 10.2.0(jiti@2.6.1) string-ts: 2.3.1 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@eslint-react/core@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/core@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: - '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.1.0(jiti@2.6.1) + '@eslint-react/ast': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/shared': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/var': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + eslint: 10.2.0(jiti@2.6.1) ts-pattern: 5.9.0 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@eslint-react/eslint-plugin@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/eslint-plugin@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: - '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.1.0(jiti@2.6.1) - eslint-plugin-react-dom: 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-naming-convention: 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-rsc: 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-web-api: 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-x: 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + '@eslint-react/shared': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/type-utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + eslint: 10.2.0(jiti@2.6.1) + eslint-plugin-react-dom: 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + eslint-plugin-react-naming-convention: 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + eslint-plugin-react-rsc: 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + eslint-plugin-react-web-api: 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + eslint-plugin-react-x: 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@eslint-react/shared@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/shared@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.1.0(jiti@2.6.1) + '@typescript-eslint/utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + eslint: 10.2.0(jiti@2.6.1) ts-pattern: 5.9.0 - typescript: 5.9.3 + typescript: 6.0.2 zod: 4.3.6 transitivePeerDependencies: - supports-color - '@eslint-react/var@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/var@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: - '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.1.0(jiti@2.6.1) + '@eslint-react/ast': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/shared': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + eslint: 10.2.0(jiti@2.6.1) ts-pattern: 5.9.0 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@eslint/compat@2.0.3(eslint@10.1.0(jiti@2.6.1))': + '@eslint/compat@2.0.3(eslint@10.2.0(jiti@2.6.1))': dependencies: - '@eslint/core': 1.1.1 + '@eslint/core': 1.2.0 optionalDependencies: - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) '@eslint/config-array@0.20.1': dependencies: @@ -9704,9 +9493,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/config-array@0.23.3': + '@eslint/config-array@0.23.4': dependencies: - '@eslint/object-schema': 3.0.3 + '@eslint/object-schema': 3.0.4 debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.4 transitivePeerDependencies: @@ -9714,9 +9503,9 @@ snapshots: '@eslint/config-helpers@0.2.3': {} - '@eslint/config-helpers@0.5.3': + '@eslint/config-helpers@0.5.4': dependencies: - '@eslint/core': 1.1.1 + '@eslint/core': 1.2.0 '@eslint/core@0.14.0': dependencies: @@ -9730,7 +9519,7 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/core@1.1.1': + '@eslint/core@1.2.0': dependencies: '@types/json-schema': 7.0.15 @@ -9753,9 +9542,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@10.0.1(eslint@10.1.0(jiti@2.6.1))': + '@eslint/js@10.0.1(eslint@10.2.0(jiti@2.6.1))': optionalDependencies: - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) '@eslint/js@9.27.0': {} @@ -9773,9 +9562,25 @@ snapshots: transitivePeerDependencies: - supports-color + '@eslint/markdown@8.0.1': + dependencies: + '@eslint/core': 1.2.0 + '@eslint/plugin-kit': 0.6.1 + github-slugger: 2.0.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-frontmatter: 2.0.1 + mdast-util-gfm: 3.1.0 + mdast-util-math: 3.0.0 + micromark-extension-frontmatter: 2.0.0 + micromark-extension-gfm: 3.0.0 + micromark-extension-math: 3.1.0 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + '@eslint/object-schema@2.1.7': {} - '@eslint/object-schema@3.0.3': {} + '@eslint/object-schema@3.0.4': {} '@eslint/plugin-kit@0.3.5': dependencies: @@ -9789,7 +9594,12 @@ snapshots: '@eslint/plugin-kit@0.6.1': dependencies: - '@eslint/core': 1.1.1 + '@eslint/core': 1.2.0 + levn: 0.4.1 + + '@eslint/plugin-kit@0.7.0': + dependencies: + '@eslint/core': 1.2.0 levn: 0.4.1 '@floating-ui/core@1.7.5': @@ -9831,7 +9641,7 @@ snapshots: dependencies: '@formatjs/fast-memoize': 3.1.1 - '@headlessui/react@2.2.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@headlessui/react@2.2.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@floating-ui/react': 0.26.28(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/focus': 3.21.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -9841,15 +9651,13 @@ snapshots: react-dom: 19.2.4(react@19.2.4) use-sync-external-store: 1.6.0(react@19.2.4) - '@henrygd/queue@1.2.0': {} - '@heroicons/react@2.2.0(react@19.2.4)': dependencies: react: 19.2.4 - '@hono/node-server@1.19.11(hono@4.12.9)': + '@hono/node-server@1.19.13(hono@4.12.12)': dependencies: - hono: 4.12.9 + hono: 4.12.12 '@humanfs/core@0.19.1': {} @@ -10005,13 +9813,13 @@ snapshots: dependencies: minipass: 7.1.3 - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)': + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)': dependencies: glob: 13.0.6 - react-docgen-typescript: 2.4.0(typescript@5.9.3) - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + react-docgen-typescript: 2.4.0(typescript@6.0.2) + vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.2 '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -10194,12 +10002,12 @@ snapshots: lexical: 0.42.0 yjs: 13.6.30 - '@mdx-js/loader@3.1.1(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': + '@mdx-js/loader@3.1.1(webpack@5.105.4(uglify-js@3.19.3))': dependencies: '@mdx-js/mdx': 3.1.1 source-map: 0.7.6 optionalDependencies: - webpack: 5.105.4(esbuild@0.27.2)(uglify-js@3.19.3) + webpack: 5.105.4(uglify-js@3.19.3) transitivePeerDependencies: - supports-color @@ -10249,7 +10057,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@mermaid-js/parser@1.0.1': + '@mermaid-js/parser@1.1.0': dependencies: langium: 4.2.1 @@ -10275,41 +10083,41 @@ snapshots: '@next/env@16.0.0': {} - '@next/env@16.2.1': {} + '@next/env@16.2.2': {} - '@next/eslint-plugin-next@16.2.1': + '@next/eslint-plugin-next@16.2.2': dependencies: fast-glob: 3.3.1 - '@next/mdx@16.2.1(@mdx-js/loader@3.1.1(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.4))': + '@next/mdx@16.2.2(@mdx-js/loader@3.1.1(webpack@5.105.4(uglify-js@3.19.3)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.4))': dependencies: source-map: 0.7.6 optionalDependencies: - '@mdx-js/loader': 3.1.1(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) + '@mdx-js/loader': 3.1.1(webpack@5.105.4(uglify-js@3.19.3)) '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.4) - '@next/swc-darwin-arm64@16.2.1': + '@next/swc-darwin-arm64@16.2.2': optional: true - '@next/swc-darwin-x64@16.2.1': + '@next/swc-darwin-x64@16.2.2': optional: true - '@next/swc-linux-arm64-gnu@16.2.1': + '@next/swc-linux-arm64-gnu@16.2.2': optional: true - '@next/swc-linux-arm64-musl@16.2.1': + '@next/swc-linux-arm64-musl@16.2.2': optional: true - '@next/swc-linux-x64-gnu@16.2.1': + '@next/swc-linux-x64-gnu@16.2.2': optional: true - '@next/swc-linux-x64-musl@16.2.1': + '@next/swc-linux-x64-musl@16.2.2': optional: true - '@next/swc-win32-arm64-msvc@16.2.1': + '@next/swc-win32-arm64-msvc@16.2.2': optional: true - '@next/swc-win32-x64-msvc@16.2.1': + '@next/swc-win32-x64-msvc@16.2.2': optional: true '@nodelib/fs.scandir@2.1.5': @@ -10382,11 +10190,11 @@ snapshots: transitivePeerDependencies: - '@opentelemetry/api' - '@orpc/tanstack-query@1.13.13(@orpc/client@1.13.13)(@tanstack/query-core@5.95.2)': + '@orpc/tanstack-query@1.13.13(@orpc/client@1.13.13)(@tanstack/query-core@5.96.2)': dependencies: '@orpc/client': 1.13.13 '@orpc/shared': 1.13.13 - '@tanstack/query-core': 5.95.2 + '@tanstack/query-core': 5.96.2 transitivePeerDependencies: - '@opentelemetry/api' @@ -10457,12 +10265,14 @@ snapshots: '@oxc-parser/binding-win32-x64-msvc@0.121.0': optional: true - '@oxc-project/runtime@0.121.0': {} + '@oxc-project/runtime@0.123.0': {} '@oxc-project/types@0.121.0': {} '@oxc-project/types@0.122.0': {} + '@oxc-project/types@0.123.0': {} + '@oxc-resolver/binding-android-arm-eabi@11.19.1': optional: true @@ -10528,136 +10338,136 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.19.1': optional: true - '@oxfmt/binding-android-arm-eabi@0.42.0': + '@oxfmt/binding-android-arm-eabi@0.43.0': optional: true - '@oxfmt/binding-android-arm64@0.42.0': + '@oxfmt/binding-android-arm64@0.43.0': optional: true - '@oxfmt/binding-darwin-arm64@0.42.0': + '@oxfmt/binding-darwin-arm64@0.43.0': optional: true - '@oxfmt/binding-darwin-x64@0.42.0': + '@oxfmt/binding-darwin-x64@0.43.0': optional: true - '@oxfmt/binding-freebsd-x64@0.42.0': + '@oxfmt/binding-freebsd-x64@0.43.0': optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.42.0': + '@oxfmt/binding-linux-arm-gnueabihf@0.43.0': optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.42.0': + '@oxfmt/binding-linux-arm-musleabihf@0.43.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.42.0': + '@oxfmt/binding-linux-arm64-gnu@0.43.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.42.0': + '@oxfmt/binding-linux-arm64-musl@0.43.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.42.0': + '@oxfmt/binding-linux-ppc64-gnu@0.43.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.42.0': + '@oxfmt/binding-linux-riscv64-gnu@0.43.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.42.0': + '@oxfmt/binding-linux-riscv64-musl@0.43.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.42.0': + '@oxfmt/binding-linux-s390x-gnu@0.43.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.42.0': + '@oxfmt/binding-linux-x64-gnu@0.43.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.42.0': + '@oxfmt/binding-linux-x64-musl@0.43.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.42.0': + '@oxfmt/binding-openharmony-arm64@0.43.0': optional: true - '@oxfmt/binding-win32-arm64-msvc@0.42.0': + '@oxfmt/binding-win32-arm64-msvc@0.43.0': optional: true - '@oxfmt/binding-win32-ia32-msvc@0.42.0': + '@oxfmt/binding-win32-ia32-msvc@0.43.0': optional: true - '@oxfmt/binding-win32-x64-msvc@0.42.0': + '@oxfmt/binding-win32-x64-msvc@0.43.0': optional: true - '@oxlint-tsgolint/darwin-arm64@0.17.3': + '@oxlint-tsgolint/darwin-arm64@0.20.0': optional: true - '@oxlint-tsgolint/darwin-x64@0.17.3': + '@oxlint-tsgolint/darwin-x64@0.20.0': optional: true - '@oxlint-tsgolint/linux-arm64@0.17.3': + '@oxlint-tsgolint/linux-arm64@0.20.0': optional: true - '@oxlint-tsgolint/linux-x64@0.17.3': + '@oxlint-tsgolint/linux-x64@0.20.0': optional: true - '@oxlint-tsgolint/win32-arm64@0.17.3': + '@oxlint-tsgolint/win32-arm64@0.20.0': optional: true - '@oxlint-tsgolint/win32-x64@0.17.3': + '@oxlint-tsgolint/win32-x64@0.20.0': optional: true - '@oxlint/binding-android-arm-eabi@1.57.0': + '@oxlint/binding-android-arm-eabi@1.58.0': optional: true - '@oxlint/binding-android-arm64@1.57.0': + '@oxlint/binding-android-arm64@1.58.0': optional: true - '@oxlint/binding-darwin-arm64@1.57.0': + '@oxlint/binding-darwin-arm64@1.58.0': optional: true - '@oxlint/binding-darwin-x64@1.57.0': + '@oxlint/binding-darwin-x64@1.58.0': optional: true - '@oxlint/binding-freebsd-x64@1.57.0': + '@oxlint/binding-freebsd-x64@1.58.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.57.0': + '@oxlint/binding-linux-arm-gnueabihf@1.58.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.57.0': + '@oxlint/binding-linux-arm-musleabihf@1.58.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.57.0': + '@oxlint/binding-linux-arm64-gnu@1.58.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.57.0': + '@oxlint/binding-linux-arm64-musl@1.58.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.57.0': + '@oxlint/binding-linux-ppc64-gnu@1.58.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.57.0': + '@oxlint/binding-linux-riscv64-gnu@1.58.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.57.0': + '@oxlint/binding-linux-riscv64-musl@1.58.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.57.0': + '@oxlint/binding-linux-s390x-gnu@1.58.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.57.0': + '@oxlint/binding-linux-x64-gnu@1.58.0': optional: true - '@oxlint/binding-linux-x64-musl@1.57.0': + '@oxlint/binding-linux-x64-musl@1.58.0': optional: true - '@oxlint/binding-openharmony-arm64@1.57.0': + '@oxlint/binding-openharmony-arm64@1.58.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.57.0': + '@oxlint/binding-win32-arm64-msvc@1.58.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.57.0': + '@oxlint/binding-win32-ia32-msvc@1.58.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.57.0': + '@oxlint/binding-win32-x64-msvc@1.58.0': optional: true '@parcel/watcher-android-arm64@2.5.6': @@ -10723,18 +10533,14 @@ snapshots: '@pkgr/core@0.2.9': {} - '@playwright/test@1.58.2': + '@playwright/test@1.59.1': dependencies: - playwright: 1.58.2 + playwright: 1.59.1 '@polka/url@1.0.0-next.29': {} '@preact/signals-core@1.14.0': {} - '@quansync/fs@1.0.0': - dependencies: - quansync: 1.0.0 - '@radix-ui/primitive@1.1.3': {} '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.4)': @@ -11081,7 +10887,7 @@ snapshots: '@rolldown/pluginutils@1.0.0-rc.12': {} - '@rolldown/pluginutils@1.0.0-rc.5': {} + '@rolldown/pluginutils@1.0.0-rc.13': {} '@rolldown/pluginutils@1.0.0-rc.7': {} @@ -11175,40 +10981,80 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.59.0': optional: true - '@sentry-internal/browser-utils@10.46.0': + '@sentry-internal/browser-utils@10.47.0': dependencies: - '@sentry/core': 10.46.0 + '@sentry/core': 10.47.0 - '@sentry-internal/feedback@10.46.0': + '@sentry-internal/feedback@10.47.0': dependencies: - '@sentry/core': 10.46.0 + '@sentry/core': 10.47.0 - '@sentry-internal/replay-canvas@10.46.0': + '@sentry-internal/replay-canvas@10.47.0': dependencies: - '@sentry-internal/replay': 10.46.0 - '@sentry/core': 10.46.0 + '@sentry-internal/replay': 10.47.0 + '@sentry/core': 10.47.0 - '@sentry-internal/replay@10.46.0': + '@sentry-internal/replay@10.47.0': dependencies: - '@sentry-internal/browser-utils': 10.46.0 - '@sentry/core': 10.46.0 + '@sentry-internal/browser-utils': 10.47.0 + '@sentry/core': 10.47.0 - '@sentry/browser@10.46.0': + '@sentry/browser@10.47.0': dependencies: - '@sentry-internal/browser-utils': 10.46.0 - '@sentry-internal/feedback': 10.46.0 - '@sentry-internal/replay': 10.46.0 - '@sentry-internal/replay-canvas': 10.46.0 - '@sentry/core': 10.46.0 + '@sentry-internal/browser-utils': 10.47.0 + '@sentry-internal/feedback': 10.47.0 + '@sentry-internal/replay': 10.47.0 + '@sentry-internal/replay-canvas': 10.47.0 + '@sentry/core': 10.47.0 - '@sentry/core@10.46.0': {} + '@sentry/core@10.47.0': {} - '@sentry/react@10.46.0(react@19.2.4)': + '@sentry/react@10.47.0(react@19.2.4)': dependencies: - '@sentry/browser': 10.46.0 - '@sentry/core': 10.46.0 + '@sentry/browser': 10.47.0 + '@sentry/core': 10.47.0 react: 19.2.4 + '@shikijs/core@4.0.2': + dependencies: + '@shikijs/primitive': 4.0.2 + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.5 + + '@shikijs/engine-oniguruma@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + + '@shikijs/primitive@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/themes@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + + '@shikijs/types@4.0.2': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + '@shuding/opentype.js@1.4.0-beta.0': dependencies: fflate: 0.7.4 @@ -11254,15 +11100,15 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@storybook/addon-docs@10.3.3(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': + '@storybook/addon-docs@10.3.5(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(uglify-js@3.19.3))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.4) - '@storybook/csf-plugin': 10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) + '@storybook/csf-plugin': 10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(uglify-js@3.19.3)) '@storybook/icons': 2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@storybook/react-dom-shim': 10.3.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + '@storybook/react-dom-shim': 10.3.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' @@ -11271,42 +11117,41 @@ snapshots: - vite - webpack - '@storybook/addon-links@10.3.3(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + '@storybook/addon-links@10.3.5(react@19.2.4)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': dependencies: '@storybook/global': 5.0.0 - storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) optionalDependencies: react: 19.2.4 - '@storybook/addon-onboarding@10.3.3(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + '@storybook/addon-onboarding@10.3.5(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': dependencies: - storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@storybook/addon-themes@10.3.3(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + '@storybook/addon-themes@10.3.5(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': dependencies: - storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ts-dedent: 2.2.0 - '@storybook/builder-vite@10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': + '@storybook/builder-vite@10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(uglify-js@3.19.3))': dependencies: - '@storybook/csf-plugin': 10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) - storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@storybook/csf-plugin': 10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(uglify-js@3.19.3)) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ts-dedent: 2.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - esbuild - rollup - webpack - '@storybook/csf-plugin@10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': + '@storybook/csf-plugin@10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(uglify-js@3.19.3))': dependencies: - storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) unplugin: 2.3.11 optionalDependencies: - esbuild: 0.27.2 rollup: 4.59.0 - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' - webpack: 5.105.4(esbuild@0.27.2)(uglify-js@3.19.3) + vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + webpack: 5.105.4(uglify-js@3.19.3) '@storybook/global@5.0.0': {} @@ -11315,20 +11160,20 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@storybook/nextjs-vite@10.3.3(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': + '@storybook/nextjs-vite@10.3.5(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2)(webpack@5.105.4(uglify-js@3.19.3))': dependencies: - '@storybook/builder-vite': 10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) - '@storybook/react': 10.3.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) - '@storybook/react-vite': 10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) - next: 16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) + '@storybook/builder-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(uglify-js@3.19.3)) + '@storybook/react': 10.3.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2) + '@storybook/react-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2)(webpack@5.105.4(uglify-js@3.19.3)) + next: 16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.4) - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' - vite-plugin-storybook-nextjs: 3.2.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite-plugin-storybook-nextjs: 3.2.4(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2) optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -11337,27 +11182,27 @@ snapshots: - supports-color - webpack - '@storybook/react-dom-shim@10.3.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + '@storybook/react-dom-shim@10.3.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': dependencies: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@storybook/react-vite@10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': + '@storybook/react-vite@10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2)(webpack@5.105.4(uglify-js@3.19.3))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2) '@rollup/pluginutils': 5.3.0(rollup@4.59.0) - '@storybook/builder-vite': 10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) - '@storybook/react': 10.3.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + '@storybook/builder-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(uglify-js@3.19.3)) + '@storybook/react': 10.3.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2) empathic: 2.0.0 magic-string: 0.30.21 react: 19.2.4 react-docgen: 8.0.3 react-dom: 19.2.4(react@19.2.4) resolve: 1.22.11 - storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tsconfig-paths: 4.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - esbuild - rollup @@ -11365,34 +11210,34 @@ snapshots: - typescript - webpack - '@storybook/react@10.3.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)': + '@storybook/react@10.3.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 10.3.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + '@storybook/react-dom-shim': 10.3.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) react: 19.2.4 react-docgen: 8.0.3 - react-docgen-typescript: 2.4.0(typescript@5.9.3) + react-docgen-typescript: 2.4.0(typescript@6.0.2) react-dom: 19.2.4(react@19.2.4) - storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color '@streamdown/math@1.0.2(react@19.2.4)': dependencies: - katex: 0.16.44 + katex: 0.16.45 react: 19.2.4 rehype-katex: 7.0.1 remark-math: 6.0.0 transitivePeerDependencies: - supports-color - '@stylistic/eslint-plugin@5.10.0(eslint@10.1.0(jiti@2.6.1))': + '@stylistic/eslint-plugin@5.10.0(eslint@10.2.0(jiti@2.6.1))': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) - '@typescript-eslint/types': 8.57.2 - eslint: 10.1.0(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) + '@typescript-eslint/types': 8.58.1 + eslint: 10.2.0(jiti@2.6.1) eslint-visitor-keys: 4.2.1 espree: 10.4.0 estraverse: 5.3.0 @@ -11408,18 +11253,18 @@ snapshots: dependencies: tslib: 2.8.1 - '@t3-oss/env-core@0.13.11(typescript@5.9.3)(valibot@1.3.1(typescript@5.9.3))(zod@4.3.6)': + '@t3-oss/env-core@0.13.11(typescript@6.0.2)(valibot@1.3.1(typescript@6.0.2))(zod@4.3.6)': optionalDependencies: - typescript: 5.9.3 - valibot: 1.3.1(typescript@5.9.3) + typescript: 6.0.2 + valibot: 1.3.1(typescript@6.0.2) zod: 4.3.6 - '@t3-oss/env-nextjs@0.13.11(typescript@5.9.3)(valibot@1.3.1(typescript@5.9.3))(zod@4.3.6)': + '@t3-oss/env-nextjs@0.13.11(typescript@6.0.2)(valibot@1.3.1(typescript@6.0.2))(zod@4.3.6)': dependencies: - '@t3-oss/env-core': 0.13.11(typescript@5.9.3)(valibot@1.3.1(typescript@5.9.3))(zod@4.3.6) + '@t3-oss/env-core': 0.13.11(typescript@6.0.2)(valibot@1.3.1(typescript@6.0.2))(zod@4.3.6) optionalDependencies: - typescript: 5.9.3 - valibot: 1.3.1(typescript@5.9.3) + typescript: 6.0.2 + valibot: 1.3.1(typescript@6.0.2) zod: 4.3.6 '@tailwindcss/node@4.2.2': @@ -11488,7 +11333,7 @@ snapshots: '@alloc/quick-lru': 5.2.0 '@tailwindcss/node': 4.2.2 '@tailwindcss/oxide': 4.2.2 - postcss: 8.5.8 + postcss: 8.5.9 tailwindcss: 4.2.2 '@tailwindcss/typography@0.5.19(tailwindcss@4.2.2)': @@ -11496,12 +11341,12 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 4.2.2 - '@tailwindcss/vite@4.2.2(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))': + '@tailwindcss/vite@4.2.2(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))': dependencies: '@tailwindcss/node': 4.2.2 '@tailwindcss/oxide': 4.2.2 tailwindcss: 4.2.2 - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' '@tanstack/devtools-client@0.0.6': dependencies: @@ -11531,7 +11376,7 @@ snapshots: react: 19.2.4 solid-js: 1.9.11 - '@tanstack/devtools@0.11.0(csstype@3.2.3)(solid-js@1.9.11)': + '@tanstack/devtools@0.11.2(csstype@3.2.3)(solid-js@1.9.11)': dependencies: '@solid-primitives/event-listener': 2.4.5(solid-js@1.9.11) '@solid-primitives/keyboard': 1.3.5(solid-js@1.9.11) @@ -11547,26 +11392,26 @@ snapshots: - csstype - utf-8-validate - '@tanstack/eslint-plugin-query@5.95.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': + '@tanstack/eslint-plugin-query@5.96.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.1.0(jiti@2.6.1) + '@typescript-eslint/utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + eslint: 10.2.0(jiti@2.6.1) optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@tanstack/form-core@1.28.5': + '@tanstack/form-core@1.28.6': dependencies: '@tanstack/devtools-event-client': 0.4.3 '@tanstack/pacer-lite': 0.1.1 '@tanstack/store': 0.9.3 - '@tanstack/form-devtools@0.2.19(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11)': + '@tanstack/form-devtools@0.2.20(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11)': dependencies: '@tanstack/devtools-ui': 0.5.1(csstype@3.2.3)(solid-js@1.9.11) '@tanstack/devtools-utils': 0.4.0(@types/react@19.2.14)(react@19.2.4)(solid-js@1.9.11) - '@tanstack/form-core': 1.28.5 + '@tanstack/form-core': 1.28.6 clsx: 2.1.1 dayjs: 1.11.20 goober: 2.1.18(csstype@3.2.3) @@ -11580,13 +11425,13 @@ snapshots: '@tanstack/pacer-lite@0.1.1': {} - '@tanstack/query-core@5.95.2': {} + '@tanstack/query-core@5.96.2': {} - '@tanstack/query-devtools@5.95.2': {} + '@tanstack/query-devtools@5.96.2': {} - '@tanstack/react-devtools@0.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11)': + '@tanstack/react-devtools@0.10.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11)': dependencies: - '@tanstack/devtools': 0.11.0(csstype@3.2.3)(solid-js@1.9.11) + '@tanstack/devtools': 0.11.2(csstype@3.2.3)(solid-js@1.9.11) '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) react: 19.2.4 @@ -11597,10 +11442,10 @@ snapshots: - solid-js - utf-8-validate - '@tanstack/react-form-devtools@0.2.19(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11)': + '@tanstack/react-form-devtools@0.2.20(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11)': dependencies: '@tanstack/devtools-utils': 0.4.0(@types/react@19.2.14)(react@19.2.4)(solid-js@1.9.11) - '@tanstack/form-devtools': 0.2.19(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11) + '@tanstack/form-devtools': 0.2.20(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11) react: 19.2.4 transitivePeerDependencies: - '@types/react' @@ -11609,23 +11454,23 @@ snapshots: - solid-js - vue - '@tanstack/react-form@1.28.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-form@1.28.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/form-core': 1.28.5 + '@tanstack/form-core': 1.28.6 '@tanstack/react-store': 0.9.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 transitivePeerDependencies: - react-dom - '@tanstack/react-query-devtools@5.95.2(@tanstack/react-query@5.95.2(react@19.2.4))(react@19.2.4)': + '@tanstack/react-query-devtools@5.96.2(@tanstack/react-query@5.96.2(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/query-devtools': 5.95.2 - '@tanstack/react-query': 5.95.2(react@19.2.4) + '@tanstack/query-devtools': 5.96.2 + '@tanstack/react-query': 5.96.2(react@19.2.4) react: 19.2.4 - '@tanstack/react-query@5.95.2(react@19.2.4)': + '@tanstack/react-query@5.96.2(react@19.2.4)': dependencies: - '@tanstack/query-core': 5.95.2 + '@tanstack/query-core': 5.96.2 react: 19.2.4 '@tanstack/react-store@0.9.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': @@ -11681,37 +11526,37 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 - '@tsslint/cli@3.0.2(@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@5.9.3))(typescript@5.9.3)': + '@tsslint/cli@3.0.2(@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@6.0.2))(typescript@6.0.2)': dependencies: '@clack/prompts': 0.8.2 - '@tsslint/config': 3.0.2(@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@5.9.3))(typescript@5.9.3) + '@tsslint/config': 3.0.2(@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@6.0.2))(typescript@6.0.2) '@tsslint/core': 3.0.2 '@volar/language-core': 2.4.28 '@volar/language-hub': 0.0.1 '@volar/typescript': 2.4.28 minimatch: 10.2.4 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - '@tsslint/compat-eslint' - tsl - '@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@5.9.3)': + '@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@6.0.2)': dependencies: '@tsslint/types': 3.0.2 - '@typescript-eslint/parser': 8.57.2(eslint@9.27.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.1(eslint@9.27.0(jiti@2.6.1))(typescript@6.0.2) eslint: 9.27.0(jiti@2.6.1) transitivePeerDependencies: - jiti - supports-color - typescript - '@tsslint/config@3.0.2(@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@5.9.3))(typescript@5.9.3)': + '@tsslint/config@3.0.2(@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@6.0.2))(typescript@6.0.2)': dependencies: '@tsslint/types': 3.0.2 minimatch: 10.2.4 - ts-api-utils: 2.5.0(typescript@5.9.3) + ts-api-utils: 2.5.0(typescript@6.0.2) optionalDependencies: - '@tsslint/compat-eslint': 3.0.2(jiti@2.6.1)(typescript@5.9.3) + '@tsslint/compat-eslint': 3.0.2(jiti@2.6.1)(typescript@6.0.2) transitivePeerDependencies: - typescript @@ -11902,10 +11747,6 @@ snapshots: '@types/geojson@7946.0.16': {} - '@types/hast@2.3.10': - dependencies: - '@types/unist': 2.0.11 - '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -11928,7 +11769,7 @@ snapshots: '@types/negotiator@0.6.4': {} - '@types/node@25.5.0': + '@types/node@25.5.2': dependencies: undici-types: 7.18.2 @@ -11936,7 +11777,7 @@ snapshots: '@types/papaparse@5.5.2': dependencies: - '@types/node': 25.5.0 + '@types/node': 25.5.2 '@types/qs@6.15.0': {} @@ -11944,14 +11785,6 @@ snapshots: dependencies: '@types/react': 19.2.14 - '@types/react-syntax-highlighter@15.5.13': - dependencies: - '@types/react': 19.2.14 - - '@types/react-window@1.8.8': - dependencies: - '@types/react': 19.2.14 - '@types/react@19.2.14': dependencies: csstype: 3.2.3 @@ -11971,71 +11804,92 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 25.5.0 + '@types/node': 25.5.2 '@types/yauzl@2.10.3': dependencies: - '@types/node': 25.5.0 + '@types/node': 25.5.2 optional: true '@types/zen-observable@0.8.3': {} - '@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.2 - eslint: 10.1.0(jiti@2.6.1) + '@typescript-eslint/parser': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/type-utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.58.1 + eslint: 10.2.0(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.57.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: '@typescript-eslint/scope-manager': 8.57.2 '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.57.2(typescript@6.0.2) '@typescript-eslint/visitor-keys': 8.57.2 debug: 4.4.3(supports-color@8.1.1) - eslint: 10.1.0(jiti@2.6.1) - typescript: 5.9.3 + eslint: 10.2.0(jiti@2.6.1) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.57.2(eslint@9.27.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.2 + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.58.1 + debug: 4.4.3(supports-color@8.1.1) + eslint: 10.2.0(jiti@2.6.1) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.58.1(eslint@9.27.0(jiti@2.6.1))(typescript@6.0.2)': + dependencies: + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.58.1 debug: 4.4.3(supports-color@8.1.1) eslint: 9.27.0(jiti@2.6.1) - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.57.2(typescript@5.9.3)': + '@typescript-eslint/project-service@8.57.2(typescript@6.0.2)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3) - '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@6.0.2) + '@typescript-eslint/types': 8.58.1 debug: 4.4.3(supports-color@8.1.1) - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/project-service@8.58.1(typescript@6.0.2)': dependencies: - '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@6.0.2) + '@typescript-eslint/types': 8.58.1 + debug: 4.4.3(supports-color@8.1.1) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/rule-tester@8.57.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': + dependencies: + '@typescript-eslint/parser': 8.57.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/typescript-estree': 8.57.2(typescript@6.0.2) + '@typescript-eslint/utils': 8.57.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) ajv: 6.14.0 - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) json-stable-stringify-without-jsonify: 1.0.1 lodash.merge: 4.6.2 semver: 7.7.4 @@ -12048,47 +11902,84 @@ snapshots: '@typescript-eslint/types': 8.57.2 '@typescript-eslint/visitor-keys': 8.57.2 - '@typescript-eslint/tsconfig-utils@8.57.2(typescript@5.9.3)': + '@typescript-eslint/scope-manager@8.58.1': dependencies: - typescript: 5.9.3 + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/visitor-keys': 8.58.1 - '@typescript-eslint/type-utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.57.2(typescript@6.0.2)': dependencies: - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + typescript: 6.0.2 + + '@typescript-eslint/tsconfig-utils@8.58.1(typescript@6.0.2)': + dependencies: + typescript: 6.0.2 + + '@typescript-eslint/type-utils@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': + dependencies: + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) debug: 4.4.3(supports-color@8.1.1) - eslint: 10.1.0(jiti@2.6.1) - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + eslint: 10.2.0(jiti@2.6.1) + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color '@typescript-eslint/types@8.57.2': {} - '@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3)': + '@typescript-eslint/types@8.58.1': {} + + '@typescript-eslint/typescript-estree@8.57.2(typescript@6.0.2)': dependencies: - '@typescript-eslint/project-service': 8.57.2(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3) + '@typescript-eslint/project-service': 8.57.2(typescript@6.0.2) + '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@6.0.2) '@typescript-eslint/types': 8.57.2 '@typescript-eslint/visitor-keys': 8.57.2 debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.4 semver: 7.7.4 tinyglobby: 0.2.15 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.58.1(typescript@6.0.2)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) + '@typescript-eslint/project-service': 8.58.1(typescript@6.0.2) + '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@6.0.2) + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/visitor-keys': 8.58.1 + debug: 4.4.3(supports-color@8.1.1) + minimatch: 10.2.4 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.57.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.57.2 '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) - eslint: 10.1.0(jiti@2.6.1) - typescript: 5.9.3 + '@typescript-eslint/typescript-estree': 8.57.2(typescript@6.0.2) + eslint: 10.2.0(jiti@2.6.1) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.2) + eslint: 10.2.0(jiti@2.6.1) + typescript: 6.0.2 transitivePeerDependencies: - supports-color @@ -12097,36 +11988,41 @@ snapshots: '@typescript-eslint/types': 8.57.2 eslint-visitor-keys: 5.0.1 - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260329.1': + '@typescript-eslint/visitor-keys@8.58.1': + dependencies: + '@typescript-eslint/types': 8.58.1 + eslint-visitor-keys: 5.0.1 + + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260407.1': optional: true - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260329.1': + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260407.1': optional: true - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260329.1': + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260407.1': optional: true - '@typescript/native-preview-linux-arm@7.0.0-dev.20260329.1': + '@typescript/native-preview-linux-arm@7.0.0-dev.20260407.1': optional: true - '@typescript/native-preview-linux-x64@7.0.0-dev.20260329.1': + '@typescript/native-preview-linux-x64@7.0.0-dev.20260407.1': optional: true - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260329.1': + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260407.1': optional: true - '@typescript/native-preview-win32-x64@7.0.0-dev.20260329.1': + '@typescript/native-preview-win32-x64@7.0.0-dev.20260407.1': optional: true - '@typescript/native-preview@7.0.0-dev.20260329.1': + '@typescript/native-preview@7.0.0-dev.20260407.1': optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260329.1 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260329.1 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20260329.1 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260329.1 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20260329.1 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260329.1 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20260329.1 + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260407.1 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260407.1 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20260407.1 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260407.1 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20260407.1 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260407.1 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20260407.1 '@ungap/structured-clone@1.3.0': {} @@ -12134,76 +12030,75 @@ snapshots: dependencies: unpic: 4.2.2 - '@unpic/react@1.0.2(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@unpic/react@1.0.2(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@unpic/core': 1.0.3 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) optionalDependencies: - next: 16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) + next: 16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) '@upsetjs/venn.js@2.0.0': optionalDependencies: d3-selection: 3.0.0 d3-transition: 3.0.1(d3-selection@3.0.0) - '@valibot/to-json-schema@1.6.0(valibot@1.3.1(typescript@5.9.3))': + '@valibot/to-json-schema@1.6.0(valibot@1.3.1(typescript@6.0.2))': dependencies: - valibot: 1.3.1(typescript@5.9.3) + valibot: 1.3.1(typescript@6.0.2) '@vercel/og@0.8.6': dependencies: '@resvg/resvg-wasm': 2.4.0 satori: 0.16.0 - '@vitejs/devtools-kit@0.1.11(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)(ws@8.20.0)': + '@vitejs/devtools-kit@0.1.11(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(ws@8.20.0)': dependencies: - '@vitejs/devtools-rpc': 0.1.11(typescript@5.9.3)(ws@8.20.0) + '@vitejs/devtools-rpc': 0.1.11(typescript@6.0.2)(ws@8.20.0) birpc: 4.0.0 ohash: 2.0.11 - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - typescript - ws - '@vitejs/devtools-rpc@0.1.11(typescript@5.9.3)(ws@8.20.0)': + '@vitejs/devtools-rpc@0.1.11(typescript@6.0.2)(ws@8.20.0)': dependencies: birpc: 4.0.0 ohash: 2.0.11 p-limit: 7.3.0 structured-clone-es: 2.0.0 - valibot: 1.3.1(typescript@5.9.3) + valibot: 1.3.1(typescript@6.0.2) optionalDependencies: ws: 8.20.0 transitivePeerDependencies: - typescript - '@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))': + '@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' - '@vitejs/plugin-rsc@0.5.21(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(react@19.2.4)': + '@vitejs/plugin-rsc@0.5.22(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.4)': dependencies: - '@rolldown/pluginutils': 1.0.0-rc.5 + '@rolldown/pluginutils': 1.0.0-rc.13 es-module-lexer: 2.0.0 estree-walker: 3.0.3 magic-string: 0.30.21 - periscopic: 4.0.2 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - srvx: 0.11.13 + srvx: 0.11.15 strip-literal: 3.1.0 turbo-stream: 3.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' - vitefu: 1.1.2(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) + vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vitefu: 1.1.3(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) optionalDependencies: - react-server-dom-webpack: 19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) + react-server-dom-webpack: 19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(uglify-js@3.19.3)) - '@vitest/coverage-v8@4.1.1(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))': + '@vitest/coverage-v8@4.1.3(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.1 + '@vitest/utils': 4.1.3 ast-v8-to-istanbul: 1.0.0 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 @@ -12212,12 +12107,12 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vitest: '@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' - '@vitest/coverage-v8@4.1.1(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3))': + '@vitest/coverage-v8@4.1.3(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3))': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.1 + '@vitest/utils': 4.1.3 ast-v8-to-istanbul: 1.0.0 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 @@ -12226,17 +12121,17 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)' + vitest: '@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)' - '@vitest/eslint-plugin@1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': + '@vitest/eslint-plugin@1.6.14(@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.1.0(jiti@2.6.1) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + eslint: 10.2.0(jiti@2.6.1) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - typescript: 5.9.3 - vitest: '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + '@typescript-eslint/eslint-plugin': 8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + typescript: 6.0.2 + vitest: '@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - supports-color @@ -12252,7 +12147,7 @@ snapshots: dependencies: tinyrainbow: 2.0.0 - '@vitest/pretty-format@4.1.1': + '@vitest/pretty-format@4.1.3': dependencies: tinyrainbow: 3.1.0 @@ -12266,52 +12161,51 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@vitest/utils@4.1.1': + '@vitest/utils@4.1.3': dependencies: - '@vitest/pretty-format': 4.1.1 + '@vitest/pretty-format': 4.1.3 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': + '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)': dependencies: - '@oxc-project/runtime': 0.121.0 - '@oxc-project/types': 0.122.0 + '@oxc-project/runtime': 0.123.0 + '@oxc-project/types': 0.123.0 lightningcss: 1.32.0 - postcss: 8.5.8 + postcss: 8.5.9 optionalDependencies: - '@types/node': 25.5.0 - esbuild: 0.27.2 + '@types/node': 25.5.2 fsevents: 2.3.3 jiti: 2.6.1 sass: 1.98.0 terser: 5.46.1 tsx: 4.21.0 - typescript: 5.9.3 + typescript: 6.0.2 yaml: 2.8.3 - '@voidzero-dev/vite-plus-darwin-arm64@0.1.14': + '@voidzero-dev/vite-plus-darwin-arm64@0.1.16': optional: true - '@voidzero-dev/vite-plus-darwin-x64@0.1.14': + '@voidzero-dev/vite-plus-darwin-x64@0.1.16': optional: true - '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.14': + '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.16': optional: true - '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.14': + '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.16': optional: true - '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.14': + '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.16': optional: true - '@voidzero-dev/vite-plus-linux-x64-musl@0.1.14': + '@voidzero-dev/vite-plus-linux-x64-musl@0.1.16': optional: true - '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': + '@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@voidzero-dev/vite-plus-core': 0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + '@voidzero-dev/vite-plus-core': 0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) es-module-lexer: 1.7.0 obug: 2.1.1 pixelmatch: 7.1.0 @@ -12321,10 +12215,10 @@ snapshots: tinybench: 2.9.0 tinyexec: 1.0.4 tinyglobby: 0.2.15 - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' ws: 8.20.0 optionalDependencies: - '@types/node': 25.5.0 + '@types/node': 25.5.2 happy-dom: 20.8.9 transitivePeerDependencies: - '@arethetypeswrong/core' @@ -12347,11 +12241,11 @@ snapshots: - utf-8-validate - yaml - '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)': + '@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@voidzero-dev/vite-plus-core': 0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + '@voidzero-dev/vite-plus-core': 0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) es-module-lexer: 1.7.0 obug: 2.1.1 pixelmatch: 7.1.0 @@ -12361,10 +12255,10 @@ snapshots: tinybench: 2.9.0 tinyexec: 1.0.4 tinyglobby: 0.2.15 - vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) ws: 8.20.0 optionalDependencies: - '@types/node': 25.5.0 + '@types/node': 25.5.2 happy-dom: 20.8.9 transitivePeerDependencies: - '@arethetypeswrong/core' @@ -12387,10 +12281,10 @@ snapshots: - utf-8-validate - yaml - '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.14': + '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.16': optional: true - '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.14': + '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.16': optional: true '@volar/language-core@2.4.28': @@ -12429,7 +12323,7 @@ snapshots: '@vue/shared': 3.5.31 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.8 + postcss: 8.5.9 source-map-js: 1.2.1 '@vue/compiler-ssr@3.5.31': @@ -12515,6 +12409,8 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 + '@webcontainer/env@1.1.1': {} + '@xstate/fsm@1.6.5': {} '@xtuc/ieee754@1.2.0': {} @@ -12549,7 +12445,7 @@ snapshots: dayjs: 1.11.20 intersection-observer: 0.12.2 js-cookie: 3.0.5 - lodash: 4.17.23 + lodash: 4.18.0 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) react-fast-compare: 3.2.2 @@ -12660,9 +12556,10 @@ snapshots: boolbase@1.0.0: {} - brace-expansion@2.0.2: + brace-expansion@1.1.13: dependencies: balanced-match: 1.0.2 + concat-map: 0.0.1 brace-expansion@5.0.5: dependencies: @@ -12698,15 +12595,8 @@ snapshots: dependencies: run-applescript: 7.1.0 - bundle-require@5.1.0(esbuild@0.27.2): - dependencies: - esbuild: 0.27.2 - load-tsconfig: 0.2.5 - bytes@3.1.2: {} - cac@6.7.14: {} - cac@7.0.0: {} callsites@3.1.0: {} @@ -12751,16 +12641,10 @@ snapshots: character-entities-html4@2.1.0: {} - character-entities-legacy@1.1.4: {} - character-entities-legacy@3.0.0: {} - character-entities@1.2.4: {} - character-entities@2.0.2: {} - character-reference-invalid@1.1.4: {} - character-reference-invalid@2.0.1: {} check-error@2.1.3: {} @@ -12791,7 +12675,7 @@ snapshots: chevrotain-allstar@0.3.1(chevrotain@11.1.2): dependencies: chevrotain: 11.1.2 - lodash-es: 4.17.23 + lodash-es: 4.18.0 chevrotain@11.1.2: dependencies: @@ -12800,11 +12684,12 @@ snapshots: '@chevrotain/regexp-to-ast': 11.1.2 '@chevrotain/types': 11.1.2 '@chevrotain/utils': 11.1.2 - lodash-es: 4.17.23 + lodash-es: 4.18.0 chokidar@4.0.3: dependencies: readdirp: 4.1.2 + optional: true chownr@1.1.4: optional: true @@ -12853,14 +12738,14 @@ snapshots: - '@types/react' - '@types/react-dom' - code-inspector-plugin@1.4.5: + code-inspector-plugin@1.5.1: dependencies: - '@code-inspector/core': 1.4.5 - '@code-inspector/esbuild': 1.4.5 - '@code-inspector/mako': 1.4.5 - '@code-inspector/turbopack': 1.4.5 - '@code-inspector/vite': 1.4.5 - '@code-inspector/webpack': 1.4.5 + '@code-inspector/core': 1.5.1 + '@code-inspector/esbuild': 1.5.1 + '@code-inspector/mako': 1.5.1 + '@code-inspector/turbopack': 1.5.1 + '@code-inspector/vite': 1.5.1 + '@code-inspector/webpack': 1.5.1 chalk: 4.1.1 transitivePeerDependencies: - supports-color @@ -12873,8 +12758,6 @@ snapshots: color-name@1.1.4: {} - comma-separated-tokens@1.0.8: {} - comma-separated-tokens@2.0.3: {} commander@14.0.0: {} @@ -12885,8 +12768,6 @@ snapshots: commander@2.20.3: {} - commander@4.1.1: {} - commander@7.2.0: {} commander@8.3.0: {} @@ -12897,12 +12778,12 @@ snapshots: compare-versions@6.1.1: {} + concat-map@0.0.1: {} + confbox@0.1.8: {} confbox@0.2.4: {} - consola@3.4.2: {} - convert-source-map@2.0.0: {} copy-to-clipboard@3.3.3: @@ -13159,7 +13040,7 @@ snapshots: dagre-d3-es@7.0.14: dependencies: d3: 7.9.0 - lodash-es: 4.17.23 + lodash-es: 4.18.0 dayjs@1.11.20: {} @@ -13196,16 +13077,12 @@ snapshots: define-lazy-prop@3.0.0: {} - defu@6.1.4: {} - delaunator@5.1.0: dependencies: robust-predicates: 3.0.3 dequal@2.0.3: {} - destr@2.0.5: {} - detect-libc@2.1.2: {} detect-node-es@1.1.0: {} @@ -13377,94 +13254,94 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-compat-utils@0.5.1(eslint@10.1.0(jiti@2.6.1)): + eslint-compat-utils@0.5.1(eslint@10.2.0(jiti@2.6.1)): dependencies: - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) semver: 7.7.4 - eslint-config-flat-gitignore@2.3.0(eslint@10.1.0(jiti@2.6.1)): + eslint-config-flat-gitignore@2.3.0(eslint@10.2.0(jiti@2.6.1)): dependencies: - '@eslint/compat': 2.0.3(eslint@10.1.0(jiti@2.6.1)) - eslint: 10.1.0(jiti@2.6.1) + '@eslint/compat': 2.0.3(eslint@10.2.0(jiti@2.6.1)) + eslint: 10.2.0(jiti@2.6.1) - eslint-flat-config-utils@3.0.2: + eslint-flat-config-utils@3.1.0: dependencies: - '@eslint/config-helpers': 0.5.3 + '@eslint/config-helpers': 0.5.4 pathe: 2.0.3 - eslint-json-compat-utils@0.2.3(eslint@10.1.0(jiti@2.6.1))(jsonc-eslint-parser@3.1.0): + eslint-json-compat-utils@0.2.3(eslint@10.2.0(jiti@2.6.1))(jsonc-eslint-parser@3.1.0): dependencies: - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) esquery: 1.7.0 jsonc-eslint-parser: 3.1.0 - eslint-markdown@0.6.0(eslint@10.1.0(jiti@2.6.1)): + eslint-markdown@0.6.0(eslint@10.2.0(jiti@2.6.1)): dependencies: '@eslint/markdown': 7.5.1 micromark-util-normalize-identifier: 2.0.1 parse5: 8.0.0 optionalDependencies: - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) transitivePeerDependencies: - supports-color - eslint-merge-processors@2.0.0(eslint@10.1.0(jiti@2.6.1)): + eslint-merge-processors@2.0.0(eslint@10.2.0(jiti@2.6.1)): dependencies: - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) - eslint-plugin-antfu@3.2.2(eslint@10.1.0(jiti@2.6.1)): + eslint-plugin-antfu@3.2.2(eslint@10.2.0(jiti@2.6.1)): dependencies: - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) - eslint-plugin-better-tailwindcss@4.3.2(eslint@10.1.0(jiti@2.6.1))(oxlint@1.57.0(oxlint-tsgolint@0.17.3))(tailwindcss@4.2.2)(typescript@5.9.3): + eslint-plugin-better-tailwindcss@4.3.2(eslint@10.2.0(jiti@2.6.1))(oxlint@1.58.0(oxlint-tsgolint@0.20.0))(tailwindcss@4.2.2)(typescript@6.0.2): dependencies: '@eslint/css-tree': 3.6.9 - '@valibot/to-json-schema': 1.6.0(valibot@1.3.1(typescript@5.9.3)) + '@valibot/to-json-schema': 1.6.0(valibot@1.3.1(typescript@6.0.2)) enhanced-resolve: 5.20.1 jiti: 2.6.1 synckit: 0.11.12 tailwind-csstree: 0.1.5 tailwindcss: 4.2.2 tsconfig-paths-webpack-plugin: 4.2.0 - valibot: 1.3.1(typescript@5.9.3) + valibot: 1.3.1(typescript@6.0.2) optionalDependencies: - eslint: 10.1.0(jiti@2.6.1) - oxlint: 1.57.0(oxlint-tsgolint@0.17.3) + eslint: 10.2.0(jiti@2.6.1) + oxlint: 1.58.0(oxlint-tsgolint@0.20.0) transitivePeerDependencies: - '@eslint/css' - typescript - eslint-plugin-command@3.5.2(@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3))(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1)): + eslint-plugin-command@3.5.2(@typescript-eslint/rule-tester@8.57.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@typescript-eslint/typescript-estree@8.58.1(typescript@6.0.2))(@typescript-eslint/utils@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1)): dependencies: '@es-joy/jsdoccomment': 0.84.0 - '@typescript-eslint/rule-tester': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.1.0(jiti@2.6.1) + '@typescript-eslint/rule-tester': 8.57.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + eslint: 10.2.0(jiti@2.6.1) - eslint-plugin-depend@1.5.0(eslint@10.1.0(jiti@2.6.1)): + eslint-plugin-depend@1.5.0(eslint@10.2.0(jiti@2.6.1)): dependencies: empathic: 2.0.0 - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) module-replacements: 2.11.0 semver: 7.7.4 - eslint-plugin-es-x@7.8.0(eslint@10.1.0(jiti@2.6.1)): + eslint-plugin-es-x@7.8.0(eslint@10.2.0(jiti@2.6.1)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 - eslint: 10.1.0(jiti@2.6.1) - eslint-compat-utils: 0.5.1(eslint@10.1.0(jiti@2.6.1)) + eslint: 10.2.0(jiti@2.6.1) + eslint-compat-utils: 0.5.1(eslint@10.2.0(jiti@2.6.1)) - eslint-plugin-hyoban@0.14.1(eslint@10.1.0(jiti@2.6.1)): + eslint-plugin-hyoban@0.14.1(eslint@10.2.0(jiti@2.6.1)): dependencies: - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) - eslint-plugin-import-lite@0.5.2(eslint@10.1.0(jiti@2.6.1)): + eslint-plugin-import-lite@0.6.0(eslint@10.2.0(jiti@2.6.1)): dependencies: - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) - eslint-plugin-jsdoc@62.8.1(eslint@10.1.0(jiti@2.6.1)): + eslint-plugin-jsdoc@62.8.1(eslint@10.2.0(jiti@2.6.1)): dependencies: '@es-joy/jsdoccomment': 0.84.0 '@es-joy/resolve.exports': 1.2.0 @@ -13472,7 +13349,7 @@ snapshots: comment-parser: 1.4.5 debug: 4.4.3(supports-color@8.1.1) escape-string-regexp: 4.0.0 - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) espree: 11.2.0 esquery: 1.7.0 html-entities: 2.6.0 @@ -13484,27 +13361,27 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-jsonc@3.1.2(eslint@10.1.0(jiti@2.6.1)): + eslint-plugin-jsonc@3.1.2(eslint@10.2.0(jiti@2.6.1)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) - '@eslint/core': 1.1.1 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) + '@eslint/core': 1.2.0 '@eslint/plugin-kit': 0.6.1 '@ota-meshi/ast-token-store': 0.3.0 diff-sequences: 29.6.3 - eslint: 10.1.0(jiti@2.6.1) - eslint-json-compat-utils: 0.2.3(eslint@10.1.0(jiti@2.6.1))(jsonc-eslint-parser@3.1.0) + eslint: 10.2.0(jiti@2.6.1) + eslint-json-compat-utils: 0.2.3(eslint@10.2.0(jiti@2.6.1))(jsonc-eslint-parser@3.1.0) jsonc-eslint-parser: 3.1.0 natural-compare: 1.4.0 synckit: 0.11.12 transitivePeerDependencies: - '@eslint/json' - eslint-plugin-markdown-preferences@0.40.3(@eslint/markdown@7.5.1)(eslint@10.1.0(jiti@2.6.1)): + eslint-plugin-markdown-preferences@0.41.0(@eslint/markdown@8.0.1)(eslint@10.2.0(jiti@2.6.1)): dependencies: - '@eslint/markdown': 7.5.1 + '@eslint/markdown': 8.0.1 diff-sequences: 29.6.3 emoji-regex-xs: 2.0.1 - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) mdast-util-from-markdown: 2.0.3 mdast-util-frontmatter: 2.0.1 mdast-util-gfm: 3.1.0 @@ -13519,24 +13396,24 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-n@17.24.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-n@17.24.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) enhanced-resolve: 5.20.1 - eslint: 10.1.0(jiti@2.6.1) - eslint-plugin-es-x: 7.8.0(eslint@10.1.0(jiti@2.6.1)) + eslint: 10.2.0(jiti@2.6.1) + eslint-plugin-es-x: 7.8.0(eslint@10.2.0(jiti@2.6.1)) get-tsconfig: 4.13.7 globals: 15.15.0 globrex: 0.1.2 ignore: 5.3.2 semver: 7.7.4 - ts-declaration-location: 1.0.7(typescript@5.9.3) + ts-declaration-location: 1.0.7(typescript@6.0.2) transitivePeerDependencies: - typescript - eslint-plugin-no-barrel-files@1.2.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-no-barrel-files@1.2.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2): dependencies: - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) transitivePeerDependencies: - eslint - supports-color @@ -13544,19 +13421,19 @@ snapshots: eslint-plugin-no-only-tests@3.3.0: {} - eslint-plugin-perfectionist@5.7.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-perfectionist@5.7.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2): dependencies: - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.1.0(jiti@2.6.1) + '@typescript-eslint/utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + eslint: 10.2.0(jiti@2.6.1) natural-orderby: 5.0.0 transitivePeerDependencies: - supports-color - typescript - eslint-plugin-pnpm@1.6.0(eslint@10.1.0(jiti@2.6.1)): + eslint-plugin-pnpm@1.6.0(eslint@10.2.0(jiti@2.6.1)): dependencies: empathic: 2.0.0 - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) jsonc-eslint-parser: 3.1.0 pathe: 2.0.3 pnpm-workspace-yaml: 1.6.0 @@ -13564,122 +13441,111 @@ snapshots: yaml: 2.8.3 yaml-eslint-parser: 2.0.0 - eslint-plugin-react-dom@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-dom@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2): dependencies: - '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/core': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/shared': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/var': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) compare-versions: 6.1.1 - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) ts-pattern: 5.9.0 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - eslint-plugin-react-hooks@7.0.1(eslint@10.1.0(jiti@2.6.1)): + eslint-plugin-react-naming-convention@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2): dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.2 - eslint: 10.1.0(jiti@2.6.1) - hermes-parser: 0.25.1 - zod: 4.3.6 - zod-validation-error: 4.0.2(zod@4.3.6) - transitivePeerDependencies: - - supports-color - - eslint-plugin-react-naming-convention@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): - dependencies: - '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/core': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/shared': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/var': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/type-utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) compare-versions: 6.1.1 - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) string-ts: 2.3.1 ts-pattern: 5.9.0 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.5.2(eslint@10.1.0(jiti@2.6.1)): + eslint-plugin-react-refresh@0.5.2(eslint@10.2.0(jiti@2.6.1)): dependencies: - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) - eslint-plugin-react-rsc@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-rsc@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2): dependencies: - '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.1.0(jiti@2.6.1) + '@eslint-react/ast': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/shared': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/var': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/type-utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + eslint: 10.2.0(jiti@2.6.1) ts-pattern: 5.9.0 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - eslint-plugin-react-web-api@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-web-api@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2): dependencies: - '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/core': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/shared': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/var': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) birecord: 0.1.1 - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) ts-pattern: 5.9.0 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - eslint-plugin-react-x@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-x@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2): dependencies: - '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/core': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/shared': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/var': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/type-utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) compare-versions: 6.1.1 - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) string-ts: 2.3.1 - ts-api-utils: 2.5.0(typescript@5.9.3) + ts-api-utils: 2.5.0(typescript@6.0.2) ts-pattern: 5.9.0 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - eslint-plugin-regexp@3.1.0(eslint@10.1.0(jiti@2.6.1)): + eslint-plugin-regexp@3.1.0(eslint@10.2.0(jiti@2.6.1)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 comment-parser: 1.4.6 - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) jsdoc-type-pratt-parser: 7.1.1 refa: 0.12.1 regexp-ast-analysis: 0.7.1 scslre: 0.3.0 - eslint-plugin-sonarjs@4.0.2(eslint@10.1.0(jiti@2.6.1)): + eslint-plugin-sonarjs@4.0.2(eslint@10.2.0(jiti@2.6.1)): dependencies: '@eslint-community/regexpp': 4.12.2 builtin-modules: 3.3.0 bytes: 3.1.2 - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) functional-red-black-tree: 1.0.1 globals: 17.4.0 jsx-ast-utils-x: 0.1.0 @@ -13687,40 +13553,40 @@ snapshots: minimatch: 10.2.4 scslre: 0.3.0 semver: 7.7.4 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 - eslint-plugin-storybook@10.3.3(eslint@10.1.0(jiti@2.6.1))(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3): + eslint-plugin-storybook@10.3.5(eslint@10.2.0(jiti@2.6.1))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2): dependencies: - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.1.0(jiti@2.6.1) - storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@typescript-eslint/utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + eslint: 10.2.0(jiti@2.6.1) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) transitivePeerDependencies: - supports-color - typescript - eslint-plugin-toml@1.3.1(eslint@10.1.0(jiti@2.6.1)): + eslint-plugin-toml@1.3.1(eslint@10.2.0(jiti@2.6.1)): dependencies: - '@eslint/core': 1.1.1 + '@eslint/core': 1.2.0 '@eslint/plugin-kit': 0.6.1 '@ota-meshi/ast-token-store': 0.3.0 debug: 4.4.3(supports-color@8.1.1) - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) toml-eslint-parser: 1.0.3 transitivePeerDependencies: - supports-color - eslint-plugin-unicorn@63.0.0(eslint@10.1.0(jiti@2.6.1)): + eslint-plugin-unicorn@64.0.0(eslint@10.2.0(jiti@2.6.1)): dependencies: '@babel/helper-validator-identifier': 7.28.5 - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) change-case: 5.4.4 ci-info: 4.4.0 clean-regexp: 1.0.0 core-js-compat: 3.49.0 - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) find-up-simple: 1.0.1 - globals: 16.5.0 + globals: 17.4.0 indent-string: 5.0.0 is-builtin-module: 5.0.0 jsesc: 3.1.0 @@ -13730,44 +13596,44 @@ snapshots: semver: 7.7.4 strip-indent: 4.1.1 - eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1)): + eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1)): dependencies: - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - eslint-plugin-vue@10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.1.0(jiti@2.6.1)))(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@2.6.1))): + eslint-plugin-vue@10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.2.0(jiti@2.6.1)))(@typescript-eslint/parser@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.2.0(jiti@2.6.1))): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) - eslint: 10.1.0(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) + eslint: 10.2.0(jiti@2.6.1) natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 7.1.1 semver: 7.7.4 - vue-eslint-parser: 10.4.0(eslint@10.1.0(jiti@2.6.1)) + vue-eslint-parser: 10.4.0(eslint@10.2.0(jiti@2.6.1)) xml-name-validator: 4.0.0 optionalDependencies: - '@stylistic/eslint-plugin': 5.10.0(eslint@10.1.0(jiti@2.6.1)) - '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@stylistic/eslint-plugin': 5.10.0(eslint@10.2.0(jiti@2.6.1)) + '@typescript-eslint/parser': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - eslint-plugin-yml@3.3.1(eslint@10.1.0(jiti@2.6.1)): + eslint-plugin-yml@3.3.1(eslint@10.2.0(jiti@2.6.1)): dependencies: - '@eslint/core': 1.1.1 + '@eslint/core': 1.2.0 '@eslint/plugin-kit': 0.6.1 '@ota-meshi/ast-token-store': 0.3.0 debug: 4.4.3(supports-color@8.1.1) diff-sequences: 29.6.3 escape-string-regexp: 5.0.0 - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) natural-compare: 1.4.0 yaml-eslint-parser: 2.0.0 transitivePeerDependencies: - supports-color - eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.31)(eslint@10.1.0(jiti@2.6.1)): + eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.31)(eslint@10.2.0(jiti@2.6.1)): dependencies: '@vue/compiler-sfc': 3.5.31 - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) eslint-scope@5.1.1: dependencies: @@ -13792,14 +13658,14 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.1.0(jiti@2.6.1): + eslint@10.2.0(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.3 - '@eslint/config-helpers': 0.5.3 - '@eslint/core': 1.1.1 - '@eslint/plugin-kit': 0.6.1 + '@eslint/config-array': 0.23.4 + '@eslint/config-helpers': 0.5.4 + '@eslint/core': 1.2.0 + '@eslint/plugin-kit': 0.7.0 '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 @@ -13977,16 +13843,22 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-string-truncated-width@1.2.1: {} + + fast-string-width@1.1.0: + dependencies: + fast-string-truncated-width: 1.2.1 + fast-uri@3.1.0: {} + fast-wrap-ansi@0.1.6: + dependencies: + fast-string-width: 1.1.0 + fastq@1.20.1: dependencies: reusify: 1.1.0 - fault@1.0.4: - dependencies: - format: 0.2.2 - fault@2.0.1: dependencies: format: 0.2.2 @@ -14024,12 +13896,6 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 - fix-dts-default-cjs-exports@1.0.1: - dependencies: - magic-string: 0.30.21 - mlly: 1.8.2 - rollup: 4.59.0 - flat-cache@4.0.1: dependencies: flatted: 3.4.2 @@ -14063,8 +13929,6 @@ snapshots: functional-red-black-tree@1.0.1: {} - fzf@0.5.2: {} - gensync@1.0.0-beta.2: {} get-east-asian-width@1.5.0: {} @@ -14108,8 +13972,6 @@ snapshots: globals@15.15.0: {} - globals@16.5.0: {} - globals@17.4.0: {} globrex@0.1.2: {} @@ -14124,7 +13986,7 @@ snapshots: happy-dom@20.8.9: dependencies: - '@types/node': 25.5.0 + '@types/node': 25.5.2 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 entities: 7.0.1 @@ -14177,8 +14039,6 @@ snapshots: dependencies: '@types/hast': 3.0.4 - hast-util-parse-selector@2.2.5: {} - hast-util-parse-selector@4.0.0: dependencies: '@types/hast': 3.0.4 @@ -14226,6 +14086,20 @@ snapshots: transitivePeerDependencies: - supports-color + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + hast-util-to-jsx-runtime@2.3.6: dependencies: '@types/estree': 1.0.8 @@ -14267,14 +14141,6 @@ snapshots: dependencies: '@types/hast': 3.0.4 - hastscript@6.0.0: - dependencies: - '@types/hast': 2.3.10 - comma-separated-tokens: 1.0.8 - hast-util-parse-selector: 2.2.5 - property-information: 5.6.0 - space-separated-tokens: 1.1.5 - hastscript@9.0.1: dependencies: '@types/hast': 3.0.4 @@ -14283,19 +14149,9 @@ snapshots: property-information: 7.1.0 space-separated-tokens: 2.0.2 - hermes-estree@0.25.1: {} - - hermes-parser@0.25.1: - dependencies: - hermes-estree: 0.25.1 - hex-rgb@4.3.0: {} - highlight.js@10.7.3: {} - - highlightjs-vue@1.0.0: {} - - hono@4.12.9: {} + hono@4.12.12: {} hosted-git-info@9.0.2: dependencies: @@ -14326,11 +14182,11 @@ snapshots: dependencies: '@babel/runtime': 7.29.2 - i18next@25.10.10(typescript@5.9.3): + i18next@26.0.3(typescript@6.0.2): dependencies: '@babel/runtime': 7.29.2 optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.2 iconify-import-svg@0.1.2: dependencies: @@ -14359,7 +14215,8 @@ snapshots: immer@11.1.4: {} - immutable@5.1.5: {} + immutable@5.1.5: + optional: true import-fresh@3.3.1: dependencies: @@ -14390,15 +14247,8 @@ snapshots: intersection-observer@0.12.2: {} - is-alphabetical@1.0.4: {} - is-alphabetical@2.0.1: {} - is-alphanumerical@1.0.4: - dependencies: - is-alphabetical: 1.0.4 - is-decimal: 1.0.4 - is-alphanumerical@2.0.1: dependencies: is-alphabetical: 2.0.1 @@ -14408,8 +14258,6 @@ snapshots: dependencies: builtin-modules: 5.0.0 - is-decimal@1.0.4: {} - is-decimal@2.0.1: {} is-docker@3.0.0: {} @@ -14420,8 +14268,6 @@ snapshots: dependencies: is-extglob: 2.1.1 - is-hexadecimal@1.0.4: {} - is-hexadecimal@2.0.1: {} is-in-ssh@1.0.0: {} @@ -14441,10 +14287,6 @@ snapshots: is-plain-obj@4.1.0: {} - is-reference@3.0.3: - dependencies: - '@types/estree': 1.0.8 - is-stream@2.0.1: {} is-wsl@3.1.1: @@ -14470,21 +14312,19 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 25.5.0 + '@types/node': 25.5.2 merge-stream: 2.0.0 supports-color: 8.1.1 jiti@2.6.1: {} - jotai@2.19.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4): + jotai@2.19.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4): optionalDependencies: '@babel/core': 7.29.0 '@babel/template': 7.28.6 '@types/react': 19.2.14 react: 19.2.4 - joycon@3.1.1: {} - js-audio-recorder@1.0.7: {} js-base64@3.7.8: {} @@ -14533,7 +14373,7 @@ snapshots: jsx-ast-utils-x@0.1.0: {} - katex@0.16.44: + katex@0.16.45: dependencies: commander: 8.3.0 @@ -14543,7 +14383,7 @@ snapshots: khroma@2.1.0: {} - knip@6.1.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): + knip@6.3.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): dependencies: '@nodelib/fs.walk': 1.2.8 fast-glob: 3.3.3 @@ -14570,7 +14410,7 @@ snapshots: kolorist@1.8.0: {} - ky@1.14.3: {} + ky@2.0.0: {} lamejs@1.2.1: dependencies: @@ -14658,17 +14498,11 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 - lilconfig@3.1.3: {} - linebreak@1.1.0: dependencies: base64-js: 0.0.8 unicode-trie: 2.0.0 - lines-and-columns@1.2.4: {} - - load-tsconfig@0.2.5: {} - loader-runner@4.3.1: {} local-pkg@1.1.2: @@ -14681,7 +14515,7 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash-es@4.17.23: {} + lodash-es@4.18.0: {} lodash.merge@4.6.2: {} @@ -14689,7 +14523,7 @@ snapshots: lodash.sortby@4.7.0: {} - lodash@4.17.23: {} + lodash@4.18.0: {} longest-streak@3.1.0: {} @@ -14703,11 +14537,6 @@ snapshots: dependencies: tslib: 2.8.1 - lowlight@1.20.0: - dependencies: - fault: 1.0.4 - highlight.js: 10.7.3 - lru-cache@11.2.7: {} lru-cache@5.1.1: @@ -14953,17 +14782,15 @@ snapshots: mdn-data@2.23.0: {} - memoize-one@5.2.1: {} - merge-stream@2.0.0: {} merge2@1.4.1: {} - mermaid@11.13.0: + mermaid@11.14.0: dependencies: '@braintree/sanitize-url': 7.1.2 '@iconify/utils': 3.1.0 - '@mermaid-js/parser': 1.0.1 + '@mermaid-js/parser': 1.1.0 '@types/d3': 7.4.3 '@upsetjs/venn.js': 2.0.0 cytoscape: 3.33.1 @@ -14974,9 +14801,9 @@ snapshots: dagre-d3-es: 7.0.14 dayjs: 1.11.20 dompurify: 3.3.2 - katex: 0.16.44 + katex: 0.16.45 khroma: 2.1.0 - lodash-es: 4.17.23 + lodash-es: 4.18.0 marked: 16.4.2 roughjs: 4.6.6 stylis: 4.3.6 @@ -15081,7 +14908,7 @@ snapshots: dependencies: '@types/katex': 0.16.8 devlop: 1.1.0 - katex: 0.16.44 + katex: 0.16.45 micromark-factory-space: 2.0.1 micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 @@ -15289,8 +15116,6 @@ snapshots: mime@4.1.0: {} - mimic-function@5.0.1: {} - mimic-response@3.1.0: optional: true @@ -15302,7 +15127,7 @@ snapshots: minimatch@3.1.5: dependencies: - brace-expansion: 2.0.2 + brace-expansion: 1.1.13 minimist@1.2.8: {} @@ -15367,9 +15192,9 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0): + next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0): dependencies: - '@next/env': 16.2.1 + '@next/env': 16.2.2 '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.10.12 caniuse-lite: 1.0.30001781 @@ -15378,15 +15203,15 @@ snapshots: react-dom: 19.2.4(react@19.2.4) styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.4) optionalDependencies: - '@next/swc-darwin-arm64': 16.2.1 - '@next/swc-darwin-x64': 16.2.1 - '@next/swc-linux-arm64-gnu': 16.2.1 - '@next/swc-linux-arm64-musl': 16.2.1 - '@next/swc-linux-x64-gnu': 16.2.1 - '@next/swc-linux-x64-musl': 16.2.1 - '@next/swc-win32-arm64-msvc': 16.2.1 - '@next/swc-win32-x64-msvc': 16.2.1 - '@playwright/test': 1.58.2 + '@next/swc-darwin-arm64': 16.2.2 + '@next/swc-darwin-x64': 16.2.2 + '@next/swc-linux-arm64-gnu': 16.2.2 + '@next/swc-linux-arm64-musl': 16.2.2 + '@next/swc-linux-x64-gnu': 16.2.2 + '@next/swc-linux-x64-musl': 16.2.2 + '@next/swc-win32-arm64-msvc': 16.2.2 + '@next/swc-win32-x64-msvc': 16.2.2 + '@playwright/test': 1.59.1 sass: 1.98.0 sharp: 0.34.5 transitivePeerDependencies: @@ -15406,8 +15231,6 @@ snapshots: node-addon-api@7.1.1: optional: true - node-fetch-native@1.6.7: {} - node-releases@2.0.36: {} normalize-package-data@8.0.0: @@ -15422,12 +15245,12 @@ snapshots: dependencies: boolbase: 1.0.0 - nuqs@2.8.9(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react@19.2.4): + nuqs@2.8.9(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react@19.2.4): dependencies: '@standard-schema/spec': 1.0.0 react: 19.2.4 optionalDependencies: - next: 16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) + next: 16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) object-assign@4.1.1: {} @@ -15435,21 +15258,19 @@ snapshots: obug@2.1.1: {} - ofetch@1.5.1: - dependencies: - destr: 2.0.5 - node-fetch-native: 1.6.7 - ufo: 1.6.3 - ohash@2.0.11: {} once@1.4.0: dependencies: wrappy: 1.0.2 - onetime@7.0.0: + oniguruma-parser@0.12.1: {} + + oniguruma-to-es@4.3.5: dependencies: - mimic-function: 5.0.1 + oniguruma-parser: 0.12.1 + regex: 6.1.0 + regex-recursion: 6.0.2 open@10.2.0: dependencies: @@ -15532,61 +15353,61 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - oxfmt@0.42.0: + oxfmt@0.43.0: dependencies: tinypool: 2.1.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.42.0 - '@oxfmt/binding-android-arm64': 0.42.0 - '@oxfmt/binding-darwin-arm64': 0.42.0 - '@oxfmt/binding-darwin-x64': 0.42.0 - '@oxfmt/binding-freebsd-x64': 0.42.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.42.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.42.0 - '@oxfmt/binding-linux-arm64-gnu': 0.42.0 - '@oxfmt/binding-linux-arm64-musl': 0.42.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.42.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.42.0 - '@oxfmt/binding-linux-riscv64-musl': 0.42.0 - '@oxfmt/binding-linux-s390x-gnu': 0.42.0 - '@oxfmt/binding-linux-x64-gnu': 0.42.0 - '@oxfmt/binding-linux-x64-musl': 0.42.0 - '@oxfmt/binding-openharmony-arm64': 0.42.0 - '@oxfmt/binding-win32-arm64-msvc': 0.42.0 - '@oxfmt/binding-win32-ia32-msvc': 0.42.0 - '@oxfmt/binding-win32-x64-msvc': 0.42.0 + '@oxfmt/binding-android-arm-eabi': 0.43.0 + '@oxfmt/binding-android-arm64': 0.43.0 + '@oxfmt/binding-darwin-arm64': 0.43.0 + '@oxfmt/binding-darwin-x64': 0.43.0 + '@oxfmt/binding-freebsd-x64': 0.43.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.43.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.43.0 + '@oxfmt/binding-linux-arm64-gnu': 0.43.0 + '@oxfmt/binding-linux-arm64-musl': 0.43.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.43.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.43.0 + '@oxfmt/binding-linux-riscv64-musl': 0.43.0 + '@oxfmt/binding-linux-s390x-gnu': 0.43.0 + '@oxfmt/binding-linux-x64-gnu': 0.43.0 + '@oxfmt/binding-linux-x64-musl': 0.43.0 + '@oxfmt/binding-openharmony-arm64': 0.43.0 + '@oxfmt/binding-win32-arm64-msvc': 0.43.0 + '@oxfmt/binding-win32-ia32-msvc': 0.43.0 + '@oxfmt/binding-win32-x64-msvc': 0.43.0 - oxlint-tsgolint@0.17.3: + oxlint-tsgolint@0.20.0: optionalDependencies: - '@oxlint-tsgolint/darwin-arm64': 0.17.3 - '@oxlint-tsgolint/darwin-x64': 0.17.3 - '@oxlint-tsgolint/linux-arm64': 0.17.3 - '@oxlint-tsgolint/linux-x64': 0.17.3 - '@oxlint-tsgolint/win32-arm64': 0.17.3 - '@oxlint-tsgolint/win32-x64': 0.17.3 + '@oxlint-tsgolint/darwin-arm64': 0.20.0 + '@oxlint-tsgolint/darwin-x64': 0.20.0 + '@oxlint-tsgolint/linux-arm64': 0.20.0 + '@oxlint-tsgolint/linux-x64': 0.20.0 + '@oxlint-tsgolint/win32-arm64': 0.20.0 + '@oxlint-tsgolint/win32-x64': 0.20.0 - oxlint@1.57.0(oxlint-tsgolint@0.17.3): + oxlint@1.58.0(oxlint-tsgolint@0.20.0): optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.57.0 - '@oxlint/binding-android-arm64': 1.57.0 - '@oxlint/binding-darwin-arm64': 1.57.0 - '@oxlint/binding-darwin-x64': 1.57.0 - '@oxlint/binding-freebsd-x64': 1.57.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.57.0 - '@oxlint/binding-linux-arm-musleabihf': 1.57.0 - '@oxlint/binding-linux-arm64-gnu': 1.57.0 - '@oxlint/binding-linux-arm64-musl': 1.57.0 - '@oxlint/binding-linux-ppc64-gnu': 1.57.0 - '@oxlint/binding-linux-riscv64-gnu': 1.57.0 - '@oxlint/binding-linux-riscv64-musl': 1.57.0 - '@oxlint/binding-linux-s390x-gnu': 1.57.0 - '@oxlint/binding-linux-x64-gnu': 1.57.0 - '@oxlint/binding-linux-x64-musl': 1.57.0 - '@oxlint/binding-openharmony-arm64': 1.57.0 - '@oxlint/binding-win32-arm64-msvc': 1.57.0 - '@oxlint/binding-win32-ia32-msvc': 1.57.0 - '@oxlint/binding-win32-x64-msvc': 1.57.0 - oxlint-tsgolint: 0.17.3 + '@oxlint/binding-android-arm-eabi': 1.58.0 + '@oxlint/binding-android-arm64': 1.58.0 + '@oxlint/binding-darwin-arm64': 1.58.0 + '@oxlint/binding-darwin-x64': 1.58.0 + '@oxlint/binding-freebsd-x64': 1.58.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.58.0 + '@oxlint/binding-linux-arm-musleabihf': 1.58.0 + '@oxlint/binding-linux-arm64-gnu': 1.58.0 + '@oxlint/binding-linux-arm64-musl': 1.58.0 + '@oxlint/binding-linux-ppc64-gnu': 1.58.0 + '@oxlint/binding-linux-riscv64-gnu': 1.58.0 + '@oxlint/binding-linux-riscv64-musl': 1.58.0 + '@oxlint/binding-linux-s390x-gnu': 1.58.0 + '@oxlint/binding-linux-x64-gnu': 1.58.0 + '@oxlint/binding-linux-x64-musl': 1.58.0 + '@oxlint/binding-openharmony-arm64': 1.58.0 + '@oxlint/binding-win32-arm64-msvc': 1.58.0 + '@oxlint/binding-win32-ia32-msvc': 1.58.0 + '@oxlint/binding-win32-x64-msvc': 1.58.0 + oxlint-tsgolint: 0.20.0 p-limit@3.1.0: dependencies: @@ -15619,15 +15440,6 @@ snapshots: color-name: 1.1.4 hex-rgb: 4.3.0 - parse-entities@2.0.0: - dependencies: - character-entities: 1.2.4 - character-entities-legacy: 1.1.4 - character-reference-invalid: 1.1.4 - is-alphanumerical: 1.0.4 - is-decimal: 1.0.4 - is-hexadecimal: 1.0.4 - parse-entities@4.0.2: dependencies: '@types/unist': 2.0.11 @@ -15700,12 +15512,6 @@ snapshots: perfect-debounce@2.1.0: {} - periscopic@4.0.2: - dependencies: - '@types/estree': 1.0.8 - is-reference: 3.0.3 - zimmerframe: 1.1.4 - picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -15714,8 +15520,6 @@ snapshots: pinyin-pro@3.28.0: {} - pirates@4.0.7: {} - pixelmatch@7.1.0: dependencies: pngjs: 7.0.0 @@ -15732,11 +15536,11 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 - playwright-core@1.58.2: {} + playwright-core@1.59.1: {} - playwright@1.58.2: + playwright@1.59.1: dependencies: - playwright-core: 1.58.2 + playwright-core: 1.59.1 optionalDependencies: fsevents: 2.3.2 @@ -15762,15 +15566,6 @@ snapshots: transitivePeerDependencies: - supports-color - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.3): - dependencies: - lilconfig: 3.1.3 - optionalDependencies: - jiti: 2.6.1 - postcss: 8.5.8 - tsx: 4.21.0 - yaml: 2.8.3 - postcss-selector-parser@6.0.10: dependencies: cssesc: 3.0.0 @@ -15789,7 +15584,7 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.8: + postcss@8.5.9: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -15821,8 +15616,6 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 - prismjs@1.30.0: {} - progress@2.0.3: {} prop-types@15.8.1: @@ -15833,10 +15626,6 @@ snapshots: property-expr@2.0.6: {} - property-information@5.6.0: - dependencies: - xtend: 4.0.2 - property-information@7.1.0: {} pump@3.0.4: @@ -15856,8 +15645,6 @@ snapshots: quansync@0.2.11: {} - quansync@1.0.0: {} - queue-microtask@1.2.3: {} radash@12.1.1: {} @@ -15880,9 +15667,9 @@ snapshots: prop-types: 15.8.1 react: 19.2.4 - react-docgen-typescript@2.4.0(typescript@5.9.3): + react-docgen-typescript@2.4.0(typescript@6.0.2): dependencies: - typescript: 5.9.3 + typescript: 6.0.2 react-docgen@8.0.3: dependencies: @@ -15929,16 +15716,16 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - react-i18next@16.6.6(i18next@25.10.10(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3): + react-i18next@17.0.2(i18next@26.0.3(typescript@6.0.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@6.0.2): dependencies: '@babel/runtime': 7.29.2 html-parse-stringify: 3.0.1 - i18next: 25.10.10(typescript@5.9.3) + i18next: 26.0.3(typescript@6.0.2) react: 19.2.4 use-sync-external-store: 1.6.0(react@19.2.4) optionalDependencies: react-dom: 19.2.4(react@19.2.4) - typescript: 5.9.3 + typescript: 6.0.2 react-is@16.13.1: {} @@ -15989,13 +15776,13 @@ snapshots: react-draggable: 4.5.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tslib: 2.6.2 - react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)): + react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(uglify-js@3.19.3)): dependencies: acorn-loose: 8.5.2 neo-async: 2.6.2 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - webpack: 5.105.4(esbuild@0.27.2)(uglify-js@3.19.3) + webpack: 5.105.4(uglify-js@3.19.3) webpack-sources: 3.3.4 react-sortablejs@6.1.4(@types/sortablejs@1.15.9)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sortablejs@1.15.7): @@ -16015,16 +15802,6 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 - react-syntax-highlighter@15.6.6(react@19.2.4): - dependencies: - '@babel/runtime': 7.29.2 - highlight.js: 10.7.3 - highlightjs-vue: 1.0.0 - lowlight: 1.20.0 - prismjs: 1.30.0 - react: 19.2.4 - refractor: 3.6.0 - react-textarea-autosize@8.5.9(@types/react@19.2.14)(react@19.2.4): dependencies: '@babel/runtime': 7.29.2 @@ -16034,13 +15811,6 @@ snapshots: transitivePeerDependencies: - '@types/react' - react-window@1.8.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4): - dependencies: - '@babel/runtime': 7.29.2 - memoize-one: 5.2.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react@19.2.4: {} reactflow@11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): @@ -16078,7 +15848,8 @@ snapshots: util-deprecate: 1.0.2 optional: true - readdirp@4.1.2: {} + readdirp@4.1.2: + optional: true recast@0.23.11: dependencies: @@ -16128,11 +15899,15 @@ snapshots: reflect-metadata@0.2.2: {} - refractor@3.6.0: + regex-recursion@6.0.2: dependencies: - hastscript: 6.0.0 - parse-entities: 2.0.0 - prismjs: 1.30.0 + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 regexp-ast-analysis@0.7.1: dependencies: @@ -16159,7 +15934,7 @@ snapshots: '@types/katex': 0.16.8 hast-util-from-html-isomorphic: 2.0.0 hast-util-to-text: 4.0.2 - katex: 0.16.44 + katex: 0.16.45 unist-util-visit-parents: 6.0.2 vfile: 6.0.3 @@ -16261,8 +16036,6 @@ snapshots: resolve-from@4.0.0: {} - resolve-from@5.0.0: {} - resolve-pkg-maps@1.0.0: {} resolve@1.22.11: @@ -16271,11 +16044,6 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - restore-cursor@5.1.0: - dependencies: - onetime: 7.0.0 - signal-exit: 4.1.0 - reusify@1.1.0: {} robust-predicates@3.0.3: {} @@ -16342,8 +16110,6 @@ snapshots: points-on-curve: 0.2.0 points-on-path: 0.2.1 - rsc-html-stream@0.0.7: {} - run-applescript@7.1.0: {} run-parallel@1.2.0: @@ -16364,6 +16130,7 @@ snapshots: source-map-js: 1.2.1 optionalDependencies: '@parcel/watcher': 2.5.6 + optional: true satori@0.16.0: dependencies: @@ -16449,7 +16216,16 @@ snapshots: shebang-regex@3.0.0: {} - signal-exit@4.1.0: {} + shiki@4.0.2: + dependencies: + '@shikijs/core': 4.0.2 + '@shikijs/engine-javascript': 4.0.2 + '@shikijs/engine-oniguruma': 4.0.2 + '@shikijs/langs': 4.0.2 + '@shikijs/themes': 4.0.2 + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 simple-concat@1.0.1: optional: true @@ -16492,8 +16268,6 @@ snapshots: source-map@0.7.6: {} - space-separated-tokens@1.1.5: {} - space-separated-tokens@2.0.2: {} spdx-correct@3.2.0: @@ -16515,7 +16289,7 @@ snapshots: spdx-license-ids@3.0.23: {} - srvx@0.11.13: {} + srvx@0.11.15: {} stackframe@1.3.4: {} @@ -16525,7 +16299,7 @@ snapshots: std-semver@1.0.8: {} - storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -16533,6 +16307,7 @@ snapshots: '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) '@vitest/expect': 3.2.4 '@vitest/spy': 3.2.4 + '@webcontainer/env': 1.1.1 esbuild: 0.27.2 open: 10.2.0 recast: 0.23.11 @@ -16552,7 +16327,7 @@ snapshots: hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 marked: 17.0.5 - mermaid: 11.13.0 + mermaid: 11.14.0 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) rehype-harden: 1.1.8 @@ -16632,16 +16407,6 @@ snapshots: stylis@4.3.6: {} - sucrase@3.35.1: - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - commander: 4.1.1 - lines-and-columns: 1.2.4 - mz: 2.7.0 - pirates: 4.0.7 - tinyglobby: 0.2.15 - ts-interface-checker: 0.1.13 - supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -16703,31 +16468,14 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - taze@19.10.0: - dependencies: - '@antfu/ni': 28.3.0 - '@henrygd/queue': 1.2.0 - cac: 7.0.0 - find-up-simple: 1.0.1 - ofetch: 1.5.1 - package-manager-detector: 1.6.0 - pathe: 2.0.3 - pnpm-workspace-yaml: 1.6.0 - restore-cursor: 5.1.0 - tinyexec: 1.0.4 - tinyglobby: 0.2.15 - unconfig: 7.5.0 - yaml: 2.8.3 - - terser-webpack-plugin@5.4.0(esbuild@0.27.2)(uglify-js@3.19.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)): + terser-webpack-plugin@5.4.0(uglify-js@3.19.3)(webpack@5.105.4(uglify-js@3.19.3)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.46.1 - webpack: 5.105.4(esbuild@0.27.2)(uglify-js@3.19.3) + webpack: 5.105.4(uglify-js@3.19.3) optionalDependencies: - esbuild: 0.27.2 uglify-js: 3.19.3 terser@5.46.1: @@ -16755,8 +16503,6 @@ snapshots: tinybench@2.9.0: {} - tinyexec@0.3.2: {} - tinyexec@1.0.4: {} tinyglobby@0.2.15: @@ -16772,11 +16518,11 @@ snapshots: tinyspy@4.0.4: {} - tldts-core@7.0.27: {} + tldts-core@7.0.28: {} - tldts@7.0.27: + tldts@7.0.28: dependencies: - tldts-core: 7.0.27 + tldts-core: 7.0.28 to-regex-range@5.0.1: dependencies: @@ -16797,32 +16543,28 @@ snapshots: totalist@3.0.1: {} - tree-kill@1.2.2: {} - trim-lines@3.0.1: {} trough@2.2.0: {} - ts-api-utils@2.5.0(typescript@5.9.3): + ts-api-utils@2.5.0(typescript@6.0.2): dependencies: - typescript: 5.9.3 + typescript: 6.0.2 ts-debounce@4.0.0: {} - ts-declaration-location@1.0.7(typescript@5.9.3): + ts-declaration-location@1.0.7(typescript@6.0.2): dependencies: picomatch: 4.0.4 - typescript: 5.9.3 + typescript: 6.0.2 ts-dedent@2.2.0: {} - ts-interface-checker@0.1.13: {} - ts-pattern@5.9.0: {} - tsconfck@3.1.6(typescript@5.9.3): + tsconfck@3.1.6(typescript@6.0.2): optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.2 tsconfig-paths-webpack-plugin@4.2.0: dependencies: @@ -16843,34 +16585,6 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3): - dependencies: - bundle-require: 5.1.0(esbuild@0.27.2) - cac: 6.7.14 - chokidar: 4.0.3 - consola: 3.4.2 - debug: 4.4.3(supports-color@8.1.1) - esbuild: 0.27.2 - fix-dts-default-cjs-exports: 1.0.1 - joycon: 3.1.1 - picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.3) - resolve-from: 5.0.0 - rollup: 4.59.0 - source-map: 0.7.6 - sucrase: 3.35.1 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tree-kill: 1.2.2 - optionalDependencies: - postcss: 8.5.8 - typescript: 5.9.3 - transitivePeerDependencies: - - jiti - - supports-color - - tsx - - yaml - tsx@4.21.0: dependencies: esbuild: 0.27.2 @@ -16897,7 +16611,7 @@ snapshots: dependencies: tagged-tag: 1.0.0 - typescript@5.9.3: {} + typescript@6.0.2: {} ufo@1.6.3: {} @@ -16905,19 +16619,6 @@ snapshots: unbash@2.2.0: {} - unconfig-core@7.5.0: - dependencies: - '@quansync/fs': 1.0.0 - quansync: 1.0.0 - - unconfig@7.5.0: - dependencies: - '@quansync/fs': 1.0.0 - defu: 6.1.4 - jiti: 2.6.1 - quansync: 1.0.0 - unconfig-core: 7.5.0 - undici-types@7.18.2: {} undici@7.24.0: {} @@ -17059,9 +16760,9 @@ snapshots: uuid@13.0.0: {} - valibot@1.3.1(typescript@5.9.3): + valibot@1.3.1(typescript@6.0.2): optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.2 validate-npm-package-license@3.0.4: dependencies: @@ -17083,22 +16784,21 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vinext@0.0.38(21fde6c2677b0aab516df83ef1beed5d): + vinext@0.0.40(@mdx-js/rollup@3.1.1(rollup@4.59.0))(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.22(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.4))(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.4)(typescript@6.0.2): dependencies: - '@unpic/react': 1.0.2(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@unpic/react': 1.0.2(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@vercel/og': 0.8.6 - '@vitejs/plugin-react': 6.0.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) + '@vitejs/plugin-react': 6.0.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) magic-string: 0.30.21 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - rsc-html-stream: 0.0.7 - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' vite-plugin-commonjs: 0.10.4 - vite-tsconfig-paths: 6.1.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3) + vite-tsconfig-paths: 6.1.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2) optionalDependencies: '@mdx-js/rollup': 3.1.1(rollup@4.59.0) - '@vitejs/plugin-rsc': 0.5.21(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(react@19.2.4) - react-server-dom-webpack: 19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) + '@vitejs/plugin-rsc': 0.5.22(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.4) + react-server-dom-webpack: 19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(uglify-js@3.19.3)) transitivePeerDependencies: - next - supports-color @@ -17117,9 +16817,9 @@ snapshots: fast-glob: 3.3.3 magic-string: 0.30.21 - vite-plugin-inspect@12.0.0-beta.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)(ws@8.20.0): + vite-plugin-inspect@12.0.0-beta.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(ws@8.20.0): dependencies: - '@vitejs/devtools-kit': 0.1.11(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)(ws@8.20.0) + '@vitejs/devtools-kit': 0.1.11(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(ws@8.20.0) ansis: 4.2.0 error-stack-parser-es: 1.0.5 obug: 2.1.1 @@ -17128,46 +16828,43 @@ snapshots: perfect-debounce: 2.1.0 sirv: 3.0.2 unplugin-utils: 0.3.1 - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - typescript - ws - vite-plugin-storybook-nextjs@3.2.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3): + vite-plugin-storybook-nextjs@3.2.4(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2): dependencies: '@next/env': 16.0.0 image-size: 2.0.2 magic-string: 0.30.21 module-alias: 2.3.4 - next: 16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) - storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ts-dedent: 2.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' - vite-tsconfig-paths: 5.1.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3) + vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite-tsconfig-paths: 5.1.4(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2) transitivePeerDependencies: - supports-color - typescript - vite-plus@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3): + vite-plus@0.1.16(@types/node@25.5.2)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3): dependencies: - '@oxc-project/types': 0.122.0 - '@voidzero-dev/vite-plus-core': 0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) - '@voidzero-dev/vite-plus-test': 0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) - cac: 7.0.0 - cross-spawn: 7.0.6 - oxfmt: 0.42.0 - oxlint: 1.57.0(oxlint-tsgolint@0.17.3) - oxlint-tsgolint: 0.17.3 - picocolors: 1.1.1 + '@oxc-project/types': 0.123.0 + '@voidzero-dev/vite-plus-core': 0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + '@voidzero-dev/vite-plus-test': 0.1.16(@types/node@25.5.2)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + oxfmt: 0.43.0 + oxlint: 1.58.0(oxlint-tsgolint@0.20.0) + oxlint-tsgolint: 0.20.0 optionalDependencies: - '@voidzero-dev/vite-plus-darwin-arm64': 0.1.14 - '@voidzero-dev/vite-plus-darwin-x64': 0.1.14 - '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.1.14 - '@voidzero-dev/vite-plus-linux-arm64-musl': 0.1.14 - '@voidzero-dev/vite-plus-linux-x64-gnu': 0.1.14 - '@voidzero-dev/vite-plus-linux-x64-musl': 0.1.14 - '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.1.14 - '@voidzero-dev/vite-plus-win32-x64-msvc': 0.1.14 + '@voidzero-dev/vite-plus-darwin-arm64': 0.1.16 + '@voidzero-dev/vite-plus-darwin-x64': 0.1.16 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.1.16 + '@voidzero-dev/vite-plus-linux-arm64-musl': 0.1.16 + '@voidzero-dev/vite-plus-linux-x64-gnu': 0.1.16 + '@voidzero-dev/vite-plus-linux-x64-musl': 0.1.16 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.1.16 + '@voidzero-dev/vite-plus-win32-x64-msvc': 0.1.16 transitivePeerDependencies: - '@arethetypeswrong/core' - '@edge-runtime/vm' @@ -17196,26 +16893,23 @@ snapshots: - vite - yaml - vite-plus@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3): + vite-plus@0.1.16(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3): dependencies: - '@oxc-project/types': 0.122.0 - '@voidzero-dev/vite-plus-core': 0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) - '@voidzero-dev/vite-plus-test': 0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) - cac: 7.0.0 - cross-spawn: 7.0.6 - oxfmt: 0.42.0 - oxlint: 1.57.0(oxlint-tsgolint@0.17.3) - oxlint-tsgolint: 0.17.3 - picocolors: 1.1.1 + '@oxc-project/types': 0.123.0 + '@voidzero-dev/vite-plus-core': 0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + '@voidzero-dev/vite-plus-test': 0.1.16(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) + oxfmt: 0.43.0 + oxlint: 1.58.0(oxlint-tsgolint@0.20.0) + oxlint-tsgolint: 0.20.0 optionalDependencies: - '@voidzero-dev/vite-plus-darwin-arm64': 0.1.14 - '@voidzero-dev/vite-plus-darwin-x64': 0.1.14 - '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.1.14 - '@voidzero-dev/vite-plus-linux-arm64-musl': 0.1.14 - '@voidzero-dev/vite-plus-linux-x64-gnu': 0.1.14 - '@voidzero-dev/vite-plus-linux-x64-musl': 0.1.14 - '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.1.14 - '@voidzero-dev/vite-plus-win32-x64-msvc': 0.1.14 + '@voidzero-dev/vite-plus-darwin-arm64': 0.1.16 + '@voidzero-dev/vite-plus-darwin-x64': 0.1.16 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.1.16 + '@voidzero-dev/vite-plus-linux-arm64-musl': 0.1.16 + '@voidzero-dev/vite-plus-linux-x64-gnu': 0.1.16 + '@voidzero-dev/vite-plus-linux-x64-musl': 0.1.16 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.1.16 + '@voidzero-dev/vite-plus-win32-x64-msvc': 0.1.16 transitivePeerDependencies: - '@arethetypeswrong/core' - '@edge-runtime/vm' @@ -17244,37 +16938,36 @@ snapshots: - vite - yaml - vite-tsconfig-paths@5.1.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3): + vite-tsconfig-paths@5.1.4(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2): dependencies: debug: 4.4.3(supports-color@8.1.1) globrex: 0.1.2 - tsconfck: 3.1.6(typescript@5.9.3) + tsconfck: 3.1.6(typescript@6.0.2) optionalDependencies: - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - supports-color - typescript - vite-tsconfig-paths@6.1.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3): + vite-tsconfig-paths@6.1.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2): dependencies: debug: 4.4.3(supports-color@8.1.1) globrex: 0.1.2 - tsconfck: 3.1.6(typescript@5.9.3) - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + tsconfck: 3.1.6(typescript@6.0.2) + vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - supports-color - typescript - vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): + vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.8 + postcss: 8.5.9 rolldown: 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.5.0 - esbuild: 0.27.2 + '@types/node': 25.5.2 fsevents: 2.3.3 jiti: 2.6.1 sass: 1.98.0 @@ -17285,15 +16978,15 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - vitefu@1.1.2(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)): + vitefu@1.1.3(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)): optionalDependencies: - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' - vitest-canvas-mock@1.1.4(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)): + vitest-canvas-mock@1.1.4(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)): dependencies: cssfontparser: 1.2.1 moo-color: 1.0.3 - vitest: '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vitest: '@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' void-elements@3.1.0: {} @@ -17314,10 +17007,10 @@ snapshots: vscode-uri@3.1.0: {} - vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@2.6.1)): + vue-eslint-parser@10.4.0(eslint@10.2.0(jiti@2.6.1)): dependencies: debug: 4.4.3(supports-color@8.1.1) - eslint: 10.1.0(jiti@2.6.1) + eslint: 10.2.0(jiti@2.6.1) eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 espree: 11.2.0 @@ -17341,7 +17034,7 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3): + webpack@5.105.4(uglify-js@3.19.3): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -17365,7 +17058,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.2 - terser-webpack-plugin: 5.4.0(esbuild@0.27.2)(uglify-js@3.19.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) + terser-webpack-plugin: 5.4.0(uglify-js@3.19.3)(webpack@5.105.4(uglify-js@3.19.3)) watchpack: 2.5.1 webpack-sources: 3.3.4 transitivePeerDependencies: @@ -17404,8 +17097,6 @@ snapshots: xmlbuilder@15.1.1: {} - xtend@4.0.2: {} - yallist@3.1.1: {} yallist@5.0.0: {} @@ -17441,12 +17132,6 @@ snapshots: zen-observable@0.10.0: {} - zimmerframe@1.1.4: {} - - zod-validation-error@4.0.2(zod@4.3.6): - dependencies: - zod: 4.3.6 - zod@4.3.6: {} zrender@6.0.0: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index abcbff7a68..6fe023066a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,9 @@ +catalogMode: prefer trustPolicy: no-downgrade -minimumReleaseAge: 2880 +trustPolicyExclude: + - chokidar@4.0.3 + - reselect@5.1.1 + - semver@6.3.1 blockExoticSubdeps: true strictDepBuilds: true allowBuilds: @@ -15,64 +19,35 @@ packages: overrides: "@lexical/code": npm:lexical-code-no-prism@0.41.0 "@monaco-editor/loader": 1.7.0 - "@nolyfill/safe-buffer": npm:safe-buffer@^5.2.1 - array-includes: npm:@nolyfill/array-includes@^1.0.44 - array.prototype.findlast: npm:@nolyfill/array.prototype.findlast@^1.0.44 - array.prototype.findlastindex: npm:@nolyfill/array.prototype.findlastindex@^1.0.44 - array.prototype.flat: npm:@nolyfill/array.prototype.flat@^1.0.44 - array.prototype.flatmap: npm:@nolyfill/array.prototype.flatmap@^1.0.44 - array.prototype.tosorted: npm:@nolyfill/array.prototype.tosorted@^1.0.44 - assert: npm:@nolyfill/assert@^1.0.26 - brace-expansion@<2.0.2: 2.0.2 + brace-expansion@>=2.0.0 <2.0.3: 2.0.3 canvas: ^3.2.2 - devalue@<5.3.2: 5.3.2 dompurify@>=3.1.3 <=3.3.1: 3.3.2 - es-iterator-helpers: npm:@nolyfill/es-iterator-helpers@^1.0.21 esbuild@<0.27.2: 0.27.2 flatted@<=3.4.1: 3.4.2 glob@>=10.2.0 <10.5.0: 11.1.0 - hasown: npm:@nolyfill/hasown@^1.0.44 - is-arguments: npm:@nolyfill/is-arguments@^1.0.44 is-core-module: npm:@nolyfill/is-core-module@^1.0.39 - is-generator-function: npm:@nolyfill/is-generator-function@^1.0.44 - is-typed-array: npm:@nolyfill/is-typed-array@^1.0.44 - isarray: npm:@nolyfill/isarray@^1.0.44 - object.assign: npm:@nolyfill/object.assign@^1.0.44 - object.entries: npm:@nolyfill/object.entries@^1.0.44 - object.fromentries: npm:@nolyfill/object.fromentries@^1.0.44 - object.groupby: npm:@nolyfill/object.groupby@^1.0.44 - object.values: npm:@nolyfill/object.values@^1.0.44 - pbkdf2: ~3.1.5 - pbkdf2@<3.1.3: 3.1.3 + lodash@>=4.0.0 <= 4.17.23: 4.18.0 + lodash-es@>=4.0.0 <= 4.17.23: 4.18.0 picomatch@<2.3.2: 2.3.2 picomatch@>=4.0.0 <4.0.4: 4.0.4 - prismjs: ~1.30 - prismjs@<1.30.0: 1.30.0 rollup@>=4.0.0 <4.59.0: 4.59.0 safe-buffer: ^5.2.1 - safe-regex-test: npm:@nolyfill/safe-regex-test@^1.0.44 safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44 side-channel: npm:@nolyfill/side-channel@^1.0.44 smol-toml@<1.6.1: 1.6.1 solid-js: 1.9.11 string-width: ~8.2.0 - string.prototype.includes: npm:@nolyfill/string.prototype.includes@^1.0.44 - string.prototype.matchall: npm:@nolyfill/string.prototype.matchall@^1.0.44 - string.prototype.repeat: npm:@nolyfill/string.prototype.repeat@^1.0.44 - string.prototype.trimend: npm:@nolyfill/string.prototype.trimend@^1.0.44 svgo@>=3.0.0 <3.3.3: 3.3.3 tar@<=7.5.10: 7.5.11 - typed-array-buffer: npm:@nolyfill/typed-array-buffer@^1.0.44 undici@>=7.0.0 <7.24.0: 7.24.0 - vite: npm:@voidzero-dev/vite-plus-core@0.1.14 - vitest: npm:@voidzero-dev/vite-plus-test@0.1.14 - which-typed-array: npm:@nolyfill/which-typed-array@^1.0.44 + vite: npm:@voidzero-dev/vite-plus-core@0.1.16 + vitest: npm:@voidzero-dev/vite-plus-test@0.1.16 yaml@>=2.0.0 <2.8.3: 2.8.3 yauzl@<3.2.1: 3.2.1 catalog: - "@amplitude/analytics-browser": 2.38.0 - "@amplitude/plugin-session-replay-browser": 1.27.5 - "@antfu/eslint-config": 7.7.3 + "@amplitude/analytics-browser": 2.38.1 + "@amplitude/plugin-session-replay-browser": 1.27.6 + "@antfu/eslint-config": 8.0.0 "@base-ui/react": 1.3.0 "@chromatic-com/storybook": 5.1.1 "@cucumber/cucumber": 12.7.0 @@ -82,9 +57,9 @@ catalog: "@eslint/js": 10.0.1 "@floating-ui/react": 0.27.19 "@formatjs/intl-localematcher": 0.8.2 - "@headlessui/react": 2.2.9 + "@headlessui/react": 2.2.10 "@heroicons/react": 2.2.0 - "@hono/node-server": 1.19.11 + "@hono/node-server": 1.19.13 "@iconify-json/heroicons": 1.2.3 "@iconify-json/ri": 1.2.10 "@lexical/code": 0.42.0 @@ -98,34 +73,35 @@ catalog: "@mdx-js/react": 3.1.1 "@mdx-js/rollup": 3.1.1 "@monaco-editor/react": 4.7.0 - "@next/eslint-plugin-next": 16.2.1 - "@next/mdx": 16.2.1 + "@next/eslint-plugin-next": 16.2.2 + "@next/mdx": 16.2.2 "@orpc/client": 1.13.13 "@orpc/contract": 1.13.13 "@orpc/openapi-client": 1.13.13 "@orpc/tanstack-query": 1.13.13 - "@playwright/test": 1.58.2 + "@playwright/test": 1.59.1 "@remixicon/react": 4.9.0 "@rgrove/parse-xml": 4.2.0 - "@sentry/react": 10.46.0 - "@storybook/addon-docs": 10.3.3 - "@storybook/addon-links": 10.3.3 - "@storybook/addon-onboarding": 10.3.3 - "@storybook/addon-themes": 10.3.3 - "@storybook/nextjs-vite": 10.3.3 - "@storybook/react": 10.3.3 + "@sentry/react": 10.47.0 + "@storybook/addon-docs": 10.3.5 + "@storybook/addon-links": 10.3.5 + "@storybook/addon-onboarding": 10.3.5 + "@storybook/addon-themes": 10.3.5 + "@storybook/nextjs-vite": 10.3.5 + "@storybook/react": 10.3.5 "@streamdown/math": 1.0.2 "@svgdotjs/svg.js": 3.2.5 "@t3-oss/env-nextjs": 0.13.11 "@tailwindcss/postcss": 4.2.2 "@tailwindcss/typography": 0.5.19 "@tailwindcss/vite": 4.2.2 - "@tanstack/eslint-plugin-query": 5.95.2 - "@tanstack/react-devtools": 0.10.0 - "@tanstack/react-form": 1.28.5 - "@tanstack/react-form-devtools": 0.2.19 - "@tanstack/react-query": 5.95.2 - "@tanstack/react-query-devtools": 5.95.2 + "@tanstack/eslint-plugin-query": 5.96.2 + "@tanstack/react-devtools": 0.10.2 + "@tanstack/react-form": 1.28.6 + "@tanstack/react-form-devtools": 0.2.20 + "@tanstack/react-query": 5.96.2 + "@tanstack/react-query-devtools": 5.96.2 + "@tanstack/react-virtual": 3.13.23 "@testing-library/dom": 10.4.1 "@testing-library/jest-dom": 6.9.1 "@testing-library/react": 16.3.2 @@ -136,20 +112,18 @@ catalog: "@types/js-cookie": 3.0.6 "@types/js-yaml": 4.0.9 "@types/negotiator": 0.6.4 - "@types/node": 25.5.0 + "@types/node": 25.5.2 "@types/postcss-js": 4.1.0 "@types/qs": 6.15.0 "@types/react": 19.2.14 "@types/react-dom": 19.2.3 - "@types/react-syntax-highlighter": 15.5.13 - "@types/react-window": 1.8.8 "@types/sortablejs": 1.15.9 - "@typescript-eslint/eslint-plugin": 8.57.2 - "@typescript-eslint/parser": 8.57.2 - "@typescript/native-preview": 7.0.0-dev.20260329.1 + "@typescript-eslint/eslint-plugin": 8.58.1 + "@typescript-eslint/parser": 8.58.1 + "@typescript/native-preview": 7.0.0-dev.20260407.1 "@vitejs/plugin-react": 6.0.1 - "@vitejs/plugin-rsc": 0.5.21 - "@vitest/coverage-v8": 4.1.1 + "@vitejs/plugin-rsc": 0.5.22 + "@vitest/coverage-v8": 4.1.3 abcjs: 6.6.2 agentation: 3.0.2 ahooks: 3.9.7 @@ -157,7 +131,7 @@ catalog: class-variance-authority: 0.7.1 clsx: 2.1.1 cmdk: 1.1.1 - code-inspector-plugin: 1.4.5 + code-inspector-plugin: 1.5.1 copy-to-clipboard: 3.3.3 cron-parser: 5.5.0 dayjs: 1.11.20 @@ -170,45 +144,45 @@ catalog: embla-carousel-react: 8.6.0 emoji-mart: 5.6.0 es-toolkit: 1.45.1 - eslint: 10.1.0 + eslint: 10.2.0 eslint-markdown: 0.6.0 eslint-plugin-better-tailwindcss: 4.3.2 eslint-plugin-hyoban: 0.14.1 - eslint-plugin-markdown-preferences: 0.40.3 + eslint-plugin-markdown-preferences: 0.41.0 eslint-plugin-no-barrel-files: 1.2.2 - eslint-plugin-react-hooks: 7.0.1 eslint-plugin-react-refresh: 0.5.2 eslint-plugin-sonarjs: 4.0.2 - eslint-plugin-storybook: 10.3.3 + eslint-plugin-storybook: 10.3.5 fast-deep-equal: 3.1.3 foxact: 0.3.0 happy-dom: 20.8.9 - hono: 4.12.9 + hast-util-to-jsx-runtime: 2.3.6 + hono: 4.12.12 html-entities: 2.6.0 html-to-image: 1.11.13 - i18next: 25.10.10 + i18next: 26.0.3 i18next-resources-to-backend: 1.2.1 iconify-import-svg: 0.1.2 immer: 11.1.4 - jotai: 2.19.0 + jotai: 2.19.1 js-audio-recorder: 1.0.7 js-cookie: 3.0.5 js-yaml: 4.1.1 jsonschema: 1.5.0 - katex: 0.16.44 - knip: 6.1.0 - ky: 1.14.3 + katex: 0.16.45 + knip: 6.3.0 + ky: 2.0.0 lamejs: 1.2.1 lexical: 0.42.0 - mermaid: 11.13.0 + mermaid: 11.14.0 mime: 4.1.0 mitt: 3.0.1 negotiator: 1.0.0 - next: 16.2.1 + next: 16.2.2 next-themes: 0.4.6 nuqs: 2.8.9 pinyin-pro: 3.28.0 - postcss: 8.5.8 + postcss: 8.5.9 postcss-js: 5.1.0 qrcode.react: 4.2.0 qs: 6.15.0 @@ -217,42 +191,39 @@ catalog: react-dom: 19.2.4 react-easy-crop: 5.5.7 react-hotkeys-hook: 5.2.4 - react-i18next: 16.6.6 + react-i18next: 17.0.2 react-multi-email: 1.0.25 react-papaparse: 4.4.0 react-pdf-highlighter: 8.0.0-rc.0 react-server-dom-webpack: 19.2.4 react-sortablejs: 6.1.4 - react-syntax-highlighter: 15.6.6 react-textarea-autosize: 8.5.9 - react-window: 1.8.11 reactflow: 11.11.4 remark-breaks: 4.0.0 remark-directive: 4.0.0 - sass: 1.98.0 scheduler: 0.27.0 sharp: 0.34.5 + shiki: 4.0.2 sortablejs: 1.15.7 std-semver: 1.0.8 - storybook: 10.3.3 + storybook: 10.3.5 streamdown: 2.5.0 string-ts: 2.3.1 tailwind-merge: 3.5.0 tailwindcss: 4.2.2 - taze: 19.10.0 - tldts: 7.0.27 - tsup: ^8.5.1 + tldts: 7.0.28 + tsdown: 0.21.7 tsx: 4.21.0 - typescript: 5.9.3 + typescript: 6.0.2 uglify-js: 3.19.3 unist-util-visit: 5.1.0 use-context-selector: 2.0.0 uuid: 13.0.0 - vinext: 0.0.38 - vite: npm:@voidzero-dev/vite-plus-core@0.1.14 + vinext: 0.0.40 + vite: npm:@voidzero-dev/vite-plus-core@0.1.16 vite-plugin-inspect: 12.0.0-beta.1 - vite-plus: 0.1.14 - vitest: npm:@voidzero-dev/vite-plus-test@0.1.14 + vite-plus: 0.1.16 + vitest: npm:@voidzero-dev/vite-plus-test@0.1.16 vitest-canvas-mock: 1.1.4 zod: 4.3.6 zundo: 2.3.0 diff --git a/sdks/nodejs-client/package.json b/sdks/nodejs-client/package.json index d487c3abb3..da9f7353ac 100644 --- a/sdks/nodejs-client/package.json +++ b/sdks/nodejs-client/package.json @@ -45,12 +45,12 @@ "homepage": "https://dify.ai", "license": "MIT", "scripts": { - "build": "tsup", + "build": "vp pack", "lint": "eslint", "lint:fix": "eslint --fix", "type-check": "tsc -p tsconfig.json --noEmit", - "test": "vitest run", - "test:coverage": "vitest run --coverage", + "test": "vp test", + "test:coverage": "vp test --coverage", "publish:check": "./scripts/publish.sh --dry-run", "publish:npm": "./scripts/publish.sh" }, @@ -61,8 +61,8 @@ "@typescript-eslint/parser": "catalog:", "@vitest/coverage-v8": "catalog:", "eslint": "catalog:", - "tsup": "catalog:", "typescript": "catalog:", + "vite-plus": "catalog:", "vitest": "catalog:" } } diff --git a/sdks/nodejs-client/tsconfig.json b/sdks/nodejs-client/tsconfig.json index f6fb5e0555..46055447be 100644 --- a/sdks/nodejs-client/tsconfig.json +++ b/sdks/nodejs-client/tsconfig.json @@ -11,7 +11,8 @@ "strict": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, - "skipLibCheck": true + "skipLibCheck": true, + "types": ["node"] }, "include": ["src/**/*.ts", "tests/**/*.ts"] } diff --git a/sdks/nodejs-client/tsup.config.ts b/sdks/nodejs-client/tsup.config.ts deleted file mode 100644 index 522382c2a5..0000000000 --- a/sdks/nodejs-client/tsup.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from "tsup"; - -export default defineConfig({ - entry: ["src/index.ts"], - format: ["esm"], - dts: true, - clean: true, - sourcemap: true, - splitting: false, - treeshake: true, - outDir: "dist", -}); diff --git a/sdks/nodejs-client/vitest.config.ts b/sdks/nodejs-client/vite.config.ts similarity index 53% rename from sdks/nodejs-client/vitest.config.ts rename to sdks/nodejs-client/vite.config.ts index c3132e9ecf..8d89508682 100644 --- a/sdks/nodejs-client/vitest.config.ts +++ b/sdks/nodejs-client/vite.config.ts @@ -1,6 +1,17 @@ -import { defineConfig } from "vitest/config"; +import { defineConfig } from "vite-plus"; export default defineConfig({ + pack: { + entry: ["src/index.ts"], + format: ["esm"], + dts: true, + clean: true, + sourcemap: true, + // splitting: false, + treeshake: true, + outDir: "dist", + target: false, + }, test: { environment: "node", include: ["**/*.test.ts"], diff --git a/taze.config.js b/taze.config.js deleted file mode 100644 index cd5a9f8656..0000000000 --- a/taze.config.js +++ /dev/null @@ -1,15 +0,0 @@ -import { defineConfig } from 'taze' - -export default defineConfig({ - exclude: [ - // We are going to replace these - 'react-syntax-highlighter', - 'react-window', - '@types/react-window', - - // We can not upgrade these yet - 'typescript', - ], - - maturityPeriod: 2, -}) diff --git a/web/Dockerfile b/web/Dockerfile index dc23416842..030651bf27 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -25,6 +25,7 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml /app/ COPY web/package.json /app/web/ COPY e2e/package.json /app/e2e/ COPY sdks/nodejs-client/package.json /app/sdks/nodejs-client/ +COPY packages /app/packages # Use packageManager from package.json RUN corepack install diff --git a/web/Dockerfile.dockerignore b/web/Dockerfile.dockerignore index b572bd863e..115f4303fa 100644 --- a/web/Dockerfile.dockerignore +++ b/web/Dockerfile.dockerignore @@ -7,6 +7,9 @@ !web/** !e2e/ !e2e/package.json +!packages/ +!packages/**/ +!packages/**/package.json !sdks/ !sdks/nodejs-client/ !sdks/nodejs-client/package.json diff --git a/web/__mocks__/@tanstack/react-virtual.ts b/web/__mocks__/@tanstack/react-virtual.ts new file mode 100644 index 0000000000..59cca5e33f --- /dev/null +++ b/web/__mocks__/@tanstack/react-virtual.ts @@ -0,0 +1,36 @@ +import { vi } from 'vitest' + +const mockVirtualizer = ({ + count, + estimateSize, +}: { + count: number + estimateSize?: (index: number) => number +}) => { + const getSize = (index: number) => estimateSize?.(index) ?? 0 + + return { + getTotalSize: () => Array.from({ length: count }).reduce((total, _, index) => total + getSize(index), 0), + getVirtualItems: () => { + let start = 0 + + return Array.from({ length: count }).map((_, index) => { + const size = getSize(index) + const virtualItem = { + end: start + size, + index, + key: index, + size, + start, + } + + start += size + return virtualItem + }) + }, + measureElement: vi.fn(), + scrollToIndex: vi.fn(), + } +} + +export { mockVirtualizer as useVirtualizer } diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx index 0c87fd1a4d..d3f15bdf46 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx @@ -35,7 +35,7 @@ const TagManagementModal = dynamic(() => import('@/app/components/base/tag-manag ssr: false, }) -export type IAppDetailLayoutProps = { +type IAppDetailLayoutProps = { children: React.ReactNode appId: string } diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx index 26373bd42a..fb2edf0102 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx @@ -25,7 +25,7 @@ import { useAppWorkflow } from '@/service/use-workflow' import { AppModeEnum } from '@/types/app' import { asyncRunSafe } from '@/utils' -export type ICardViewProps = { +type ICardViewProps = { appId: string isInPanel?: boolean className?: string diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chart-view.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chart-view.tsx index b6e902f456..0d33de2972 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chart-view.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chart-view.tsx @@ -27,7 +27,7 @@ const TIME_PERIOD_MAPPING: { value: number, name: TimePeriodName }[] = [ const queryDateFormat = 'YYYY-MM-DD HH:mm' -export type IChartViewProps = { +type IChartViewProps = { appId: string headerRight: React.ReactNode } diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/style.module.css b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/style.module.css index 1f1aca2d11..45c7d197b4 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/style.module.css +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/style.module.css @@ -1,5 +1,3 @@ -@reference "../../../../styles/globals.css"; - .app { flex-grow: 1; height: 0; diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/documents/style.module.css b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/documents/style.module.css deleted file mode 100644 index 955f5d593b..0000000000 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/documents/style.module.css +++ /dev/null @@ -1,11 +0,0 @@ -@reference "../../../../../styles/globals.css"; - -.logTable td { - padding: 7px 8px; - box-sizing: border-box; - max-width: 200px; -} - -.pagination li { - list-style: none; -} diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx index 730b76ee19..092e47278f 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx @@ -26,7 +26,7 @@ import { usePathname } from '@/next/navigation' import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset' import { cn } from '@/utils/classnames' -export type IAppDetailLayoutProps = { +type IAppDetailLayoutProps = { children: React.ReactNode datasetId: string } diff --git a/web/app/account/(commonLayout)/avatar.tsx b/web/app/account/(commonLayout)/avatar.tsx index 5461ce739c..36a510cf63 100644 --- a/web/app/account/(commonLayout)/avatar.tsx +++ b/web/app/account/(commonLayout)/avatar.tsx @@ -13,10 +13,6 @@ import { useProviderContext } from '@/context/provider-context' import { useRouter } from '@/next/navigation' import { useLogout, useUserProfile } from '@/service/use-common' -export type IAppSelector = { - isMobile: boolean -} - export default function AppSelector() { const router = useRouter() const { t } = useTranslation() diff --git a/web/app/components/app-sidebar/app-info/index.tsx b/web/app/components/app-sidebar/app-info/index.tsx index 2530add2dc..a0628ec786 100644 --- a/web/app/components/app-sidebar/app-info/index.tsx +++ b/web/app/components/app-sidebar/app-info/index.tsx @@ -5,7 +5,7 @@ import AppInfoModals from './app-info-modals' import AppInfoTrigger from './app-info-trigger' import { useAppInfoActions } from './use-app-info-actions' -export type IAppInfoProps = { +type IAppInfoProps = { expand: boolean onlyShowDetail?: boolean openState?: boolean diff --git a/web/app/components/app-sidebar/basic.tsx b/web/app/components/app-sidebar/basic.tsx index 24746aa687..29a08f8a01 100644 --- a/web/app/components/app-sidebar/basic.tsx +++ b/web/app/components/app-sidebar/basic.tsx @@ -7,7 +7,7 @@ import { import Tooltip from '@/app/components/base/tooltip' import AppIcon from '../base/app-icon' -export type IAppBasicProps = { +type IAppBasicProps = { iconType?: 'app' | 'api' | 'dataset' | 'webapp' | 'notion' icon?: string icon_background?: string | null diff --git a/web/app/components/app-sidebar/index.tsx b/web/app/components/app-sidebar/index.tsx index bbf71c6cf9..f86cd617e3 100644 --- a/web/app/components/app-sidebar/index.tsx +++ b/web/app/components/app-sidebar/index.tsx @@ -17,7 +17,7 @@ import DatasetSidebarDropdown from './dataset-sidebar-dropdown' import NavLink from './nav-link' import ToggleButton from './toggle-button' -export type IAppDetailNavProps = { +type IAppDetailNavProps = { iconType?: 'app' | 'dataset' navigation: Array<{ name: string diff --git a/web/app/components/app/__tests__/store.spec.ts b/web/app/components/app/__tests__/store.spec.ts new file mode 100644 index 0000000000..204d659fdd --- /dev/null +++ b/web/app/components/app/__tests__/store.spec.ts @@ -0,0 +1,69 @@ +import { useStore } from '../store' + +const resetStore = () => { + useStore.setState({ + appDetail: undefined, + appSidebarExpand: '', + currentLogItem: undefined, + currentLogModalActiveTab: 'DETAIL', + showPromptLogModal: false, + showAgentLogModal: false, + showMessageLogModal: false, + showAppConfigureFeaturesModal: false, + }) +} + +describe('app store', () => { + beforeEach(() => { + resetStore() + }) + + it('should expose the default state', () => { + expect(useStore.getState()).toEqual(expect.objectContaining({ + appDetail: undefined, + appSidebarExpand: '', + currentLogItem: undefined, + currentLogModalActiveTab: 'DETAIL', + showPromptLogModal: false, + showAgentLogModal: false, + showMessageLogModal: false, + showAppConfigureFeaturesModal: false, + })) + }) + + it('should update every mutable field through its actions', () => { + const appDetail = { id: 'app-1' } as ReturnType['appDetail'] + const currentLogItem = { id: 'message-1' } as ReturnType['currentLogItem'] + + useStore.getState().setAppDetail(appDetail) + useStore.getState().setAppSidebarExpand('logs') + useStore.getState().setCurrentLogItem(currentLogItem) + useStore.getState().setCurrentLogModalActiveTab('MESSAGE') + useStore.getState().setShowPromptLogModal(true) + useStore.getState().setShowAgentLogModal(true) + useStore.getState().setShowAppConfigureFeaturesModal(true) + + expect(useStore.getState()).toEqual(expect.objectContaining({ + appDetail, + appSidebarExpand: 'logs', + currentLogItem, + currentLogModalActiveTab: 'MESSAGE', + showPromptLogModal: true, + showAgentLogModal: true, + showAppConfigureFeaturesModal: true, + })) + }) + + it('should reset the active tab when the message log modal closes', () => { + useStore.getState().setCurrentLogModalActiveTab('TRACE') + useStore.getState().setShowMessageLogModal(true) + + expect(useStore.getState().showMessageLogModal).toBe(true) + expect(useStore.getState().currentLogModalActiveTab).toBe('TRACE') + + useStore.getState().setShowMessageLogModal(false) + + expect(useStore.getState().showMessageLogModal).toBe(false) + expect(useStore.getState().currentLogModalActiveTab).toBe('DETAIL') + }) +}) diff --git a/web/app/components/app/annotation/batch-action.spec.tsx b/web/app/components/app/annotation/__tests__/batch-action.spec.tsx similarity index 96% rename from web/app/components/app/annotation/batch-action.spec.tsx rename to web/app/components/app/annotation/__tests__/batch-action.spec.tsx index 8d56dde14a..95dddd4b23 100644 --- a/web/app/components/app/annotation/batch-action.spec.tsx +++ b/web/app/components/app/annotation/__tests__/batch-action.spec.tsx @@ -1,6 +1,6 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' -import BatchAction from './batch-action' +import BatchAction from '../batch-action' describe('BatchAction', () => { const baseProps = { diff --git a/web/app/components/app/annotation/empty-element.spec.tsx b/web/app/components/app/annotation/__tests__/empty-element.spec.tsx similarity index 91% rename from web/app/components/app/annotation/empty-element.spec.tsx rename to web/app/components/app/annotation/__tests__/empty-element.spec.tsx index 89ba7e9ff8..7dcf902a9e 100644 --- a/web/app/components/app/annotation/empty-element.spec.tsx +++ b/web/app/components/app/annotation/__tests__/empty-element.spec.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react' import * as React from 'react' -import EmptyElement from './empty-element' +import EmptyElement from '../empty-element' describe('EmptyElement', () => { it('should render the empty state copy and supporting icon', () => { diff --git a/web/app/components/app/annotation/filter.spec.tsx b/web/app/components/app/annotation/__tests__/filter.spec.tsx similarity index 99% rename from web/app/components/app/annotation/filter.spec.tsx rename to web/app/components/app/annotation/__tests__/filter.spec.tsx index 7bb39bd444..8b69494e3f 100644 --- a/web/app/components/app/annotation/filter.spec.tsx +++ b/web/app/components/app/annotation/__tests__/filter.spec.tsx @@ -1,12 +1,12 @@ import type { UseQueryResult } from '@tanstack/react-query' import type { Mock } from 'vitest' -import type { QueryParam } from './filter' +import type { QueryParam } from '../filter' import type { AnnotationsCountResponse } from '@/models/log' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' import * as useLogModule from '@/service/use-log' -import Filter from './filter' +import Filter from '../filter' vi.mock('@/service/use-log') diff --git a/web/app/components/app/annotation/index.spec.tsx b/web/app/components/app/annotation/__tests__/index.spec.tsx similarity index 53% rename from web/app/components/app/annotation/index.spec.tsx rename to web/app/components/app/annotation/__tests__/index.spec.tsx index 5f5e9f74c0..cd9b127c7f 100644 --- a/web/app/components/app/annotation/index.spec.tsx +++ b/web/app/components/app/annotation/__tests__/index.spec.tsx @@ -1,5 +1,6 @@ +/* eslint-disable ts/no-explicit-any */ import type { Mock } from 'vitest' -import type { AnnotationItem } from './type' +import type { AnnotationItem } from '../type' import type { App } from '@/types/app' import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' @@ -9,13 +10,16 @@ import { addAnnotation, delAnnotation, delAnnotations, + editAnnotation, fetchAnnotationConfig, fetchAnnotationList, queryAnnotationJobStatus, + updateAnnotationScore, + updateAnnotationStatus, } from '@/service/annotation' import { AppModeEnum } from '@/types/app' -import Annotation from './index' -import { JobStatus } from './type' +import Annotation from '../index' +import { AnnotationEnableStatus, JobStatus } from '../type' vi.mock('ahooks', () => ({ useDebounce: (value: any) => value, @@ -37,29 +41,32 @@ vi.mock('@/context/provider-context', () => ({ useProviderContext: vi.fn(), })) -vi.mock('./filter', () => ({ +vi.mock('../filter', () => ({ default: ({ children }: { children: React.ReactNode }) => (
{children}
), })) -vi.mock('./empty-element', () => ({ +vi.mock('../empty-element', () => ({ default: () =>
, })) -vi.mock('./header-opts', () => ({ +vi.mock('../header-opts', () => ({ default: (props: any) => (
+
), })) let latestListProps: any -vi.mock('./list', () => ({ +vi.mock('../list', () => ({ default: (props: any) => { latestListProps = props if (!props.list.length) @@ -74,7 +81,7 @@ vi.mock('./list', () => ({ }, })) -vi.mock('./view-annotation-modal', () => ({ +vi.mock('../view-annotation-modal', () => ({ default: (props: any) => { if (!props.isShow) return null @@ -82,14 +89,40 @@ vi.mock('./view-annotation-modal', () => ({
{props.item.question}
+
) }, })) -vi.mock('@/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal', () => ({ default: (props: any) => props.isShow ?
: null })) -vi.mock('@/app/components/billing/annotation-full/modal', () => ({ default: (props: any) => props.show ?
: null })) +vi.mock('@/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal', () => ({ + default: (props: any) => props.isShow + ? ( +
+ + +
+ ) + : null, +})) +vi.mock('@/app/components/billing/annotation-full/modal', () => ({ + default: (props: any) => props.show + ? ( +
+ +
+ ) + : null, +})) const mockNotify = vi.fn() vi.spyOn(toast, 'success').mockImplementation((message, options) => { @@ -111,9 +144,12 @@ vi.spyOn(toast, 'info').mockImplementation((message, options) => { const addAnnotationMock = addAnnotation as Mock const delAnnotationMock = delAnnotation as Mock const delAnnotationsMock = delAnnotations as Mock +const editAnnotationMock = editAnnotation as Mock const fetchAnnotationConfigMock = fetchAnnotationConfig as Mock const fetchAnnotationListMock = fetchAnnotationList as Mock const queryAnnotationJobStatusMock = queryAnnotationJobStatus as Mock +const updateAnnotationScoreMock = updateAnnotationScore as Mock +const updateAnnotationStatusMock = updateAnnotationStatus as Mock const useProviderContextMock = useProviderContext as Mock const appDetail = { @@ -146,6 +182,9 @@ describe('Annotation', () => { }) fetchAnnotationListMock.mockResolvedValue({ data: [], total: 0 }) queryAnnotationJobStatusMock.mockResolvedValue({ job_status: JobStatus.completed }) + updateAnnotationStatusMock.mockResolvedValue({ job_id: 'job-1' }) + updateAnnotationScoreMock.mockResolvedValue(undefined) + editAnnotationMock.mockResolvedValue(undefined) useProviderContextMock.mockReturnValue({ plan: { usage: { annotatedResponse: 0 }, @@ -251,4 +290,166 @@ describe('Annotation', () => { expect(latestListProps.selectedIds).toEqual([annotation.id]) }) }) + + it('should show the annotation-full modal when enabling annotations exceeds the plan quota', async () => { + useProviderContextMock.mockReturnValue({ + plan: { + usage: { annotatedResponse: 10 }, + total: { annotatedResponse: 10 }, + }, + enableBilling: true, + }) + + renderComponent() + + const toggle = await screen.findByRole('switch') + fireEvent.click(toggle) + + expect(screen.getByTestId('annotation-full-modal')).toBeInTheDocument() + + fireEvent.click(screen.getByTestId('hide-annotation-full-modal')) + expect(screen.queryByTestId('annotation-full-modal')).not.toBeInTheDocument() + }) + + it('should disable annotations and refetch config after the async job completes', async () => { + fetchAnnotationConfigMock.mockResolvedValueOnce({ + id: 'config-id', + enabled: true, + embedding_model: { + embedding_model_name: 'model', + embedding_provider_name: 'provider', + }, + score_threshold: 0.5, + }).mockResolvedValueOnce({ + id: 'config-id', + enabled: false, + embedding_model: { + embedding_model_name: 'model', + embedding_provider_name: 'provider', + }, + score_threshold: 0.5, + }) + + renderComponent() + + const toggle = await screen.findByRole('switch') + await waitFor(() => { + expect(toggle).toHaveAttribute('aria-checked', 'true') + }) + fireEvent.click(toggle) + + await waitFor(() => { + expect(updateAnnotationStatusMock).toHaveBeenCalledWith( + appDetail.id, + AnnotationEnableStatus.disable, + expect.objectContaining({ + embedding_model_name: 'model', + embedding_provider_name: 'provider', + }), + 0.5, + ) + expect(queryAnnotationJobStatusMock).toHaveBeenCalledWith(appDetail.id, AnnotationEnableStatus.disable, 'job-1') + expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ + message: 'common.api.actionSuccess', + type: 'success', + })) + }) + }) + + it('should save annotation config changes and update the score when the modal confirms', async () => { + fetchAnnotationConfigMock.mockResolvedValue({ + id: 'config-id', + enabled: false, + embedding_model: { + embedding_model_name: 'model', + embedding_provider_name: 'provider', + }, + score_threshold: 0.5, + }) + + renderComponent() + + const toggle = await screen.findByRole('switch') + fireEvent.click(toggle) + + expect(screen.getByTestId('config-modal')).toBeInTheDocument() + fireEvent.click(screen.getByTestId('config-save')) + + await waitFor(() => { + expect(updateAnnotationStatusMock).toHaveBeenCalledWith( + appDetail.id, + AnnotationEnableStatus.enable, + { + embedding_model_name: 'next-model', + embedding_provider_name: 'next-provider', + }, + 0.7, + ) + expect(updateAnnotationScoreMock).toHaveBeenCalledWith(appDetail.id, 'config-id', 0.7) + expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ + message: 'common.api.actionSuccess', + type: 'success', + })) + }) + }) + + it('should refresh the list from the header shortcut and allow saving or closing the view modal', async () => { + const annotation = createAnnotation() + fetchAnnotationListMock.mockResolvedValue({ data: [annotation], total: 1 }) + + renderComponent() + + await screen.findByTestId('list') + fireEvent.click(screen.getByTestId('list-view')) + + fireEvent.click(screen.getByTestId('view-modal-save')) + + await waitFor(() => { + expect(editAnnotationMock).toHaveBeenCalledWith(appDetail.id, annotation.id, { + question: 'Edited question', + answer: 'Edited answer', + }) + }) + + fireEvent.click(screen.getByTestId('view-modal-close')) + expect(screen.queryByTestId('view-modal')).not.toBeInTheDocument() + + fireEvent.click(screen.getByTestId('trigger-added')) + + expect(fetchAnnotationListMock).toHaveBeenCalled() + }) + + it('should clear selections on cancel and hide the config modal when requested', async () => { + const annotation = createAnnotation() + fetchAnnotationConfigMock.mockResolvedValue({ + id: 'config-id', + enabled: true, + embedding_model: { + embedding_model_name: 'model', + embedding_provider_name: 'provider', + }, + score_threshold: 0.5, + }) + fetchAnnotationListMock.mockResolvedValue({ data: [annotation], total: 1 }) + + renderComponent() + + await screen.findByTestId('list') + + await act(async () => { + latestListProps.onSelectedIdsChange([annotation.id]) + }) + await act(async () => { + latestListProps.onCancel() + }) + + expect(latestListProps.selectedIds).toEqual([]) + + const configButton = document.querySelector('.action-btn') as HTMLButtonElement + fireEvent.click(configButton) + expect(await screen.findByTestId('config-modal')).toBeInTheDocument() + + fireEvent.click(screen.getByTestId('config-hide')) + expect(screen.queryByTestId('config-modal')).not.toBeInTheDocument() + }) }) diff --git a/web/app/components/app/annotation/list.spec.tsx b/web/app/components/app/annotation/__tests__/list.spec.tsx similarity index 97% rename from web/app/components/app/annotation/list.spec.tsx rename to web/app/components/app/annotation/__tests__/list.spec.tsx index c126092ecf..aa47a5304b 100644 --- a/web/app/components/app/annotation/list.spec.tsx +++ b/web/app/components/app/annotation/__tests__/list.spec.tsx @@ -1,7 +1,7 @@ -import type { AnnotationItem } from './type' +import type { AnnotationItem } from '../type' import { fireEvent, render, screen, within } from '@testing-library/react' import * as React from 'react' -import List from './list' +import List from '../list' const mockFormatTime = vi.fn(() => 'formatted-time') diff --git a/web/app/components/app/annotation/add-annotation-modal/index.spec.tsx b/web/app/components/app/annotation/add-annotation-modal/__tests__/index.spec.tsx similarity index 99% rename from web/app/components/app/annotation/add-annotation-modal/index.spec.tsx rename to web/app/components/app/annotation/add-annotation-modal/__tests__/index.spec.tsx index 14f94d910b..d7e46f9f92 100644 --- a/web/app/components/app/annotation/add-annotation-modal/index.spec.tsx +++ b/web/app/components/app/annotation/add-annotation-modal/__tests__/index.spec.tsx @@ -2,7 +2,7 @@ import type { Mock } from 'vitest' import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' import { useProviderContext } from '@/context/provider-context' -import AddAnnotationModal from './index' +import AddAnnotationModal from '../index' vi.mock('@/context/provider-context', () => ({ useProviderContext: vi.fn(), diff --git a/web/app/components/app/annotation/add-annotation-modal/edit-item/index.spec.tsx b/web/app/components/app/annotation/add-annotation-modal/edit-item/__tests__/index.spec.tsx similarity index 96% rename from web/app/components/app/annotation/add-annotation-modal/edit-item/index.spec.tsx rename to web/app/components/app/annotation/add-annotation-modal/edit-item/__tests__/index.spec.tsx index ce660f7880..6dd1d42246 100644 --- a/web/app/components/app/annotation/add-annotation-modal/edit-item/index.spec.tsx +++ b/web/app/components/app/annotation/add-annotation-modal/edit-item/__tests__/index.spec.tsx @@ -1,6 +1,6 @@ import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' -import EditItem, { EditItemType } from './index' +import EditItem, { EditItemType } from '../index' describe('AddAnnotationModal/EditItem', () => { it('should render query inputs with user avatar and placeholder strings', () => { diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/csv-downloader.spec.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-downloader.spec.tsx similarity index 95% rename from web/app/components/app/annotation/batch-add-annotation-modal/csv-downloader.spec.tsx rename to web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-downloader.spec.tsx index 2ab0934fe2..69574564eb 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/csv-downloader.spec.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-downloader.spec.tsx @@ -1,10 +1,11 @@ +/* eslint-disable ts/no-explicit-any */ import type { Mock } from 'vitest' import type { Locale } from '@/i18n-config' import { render, screen } from '@testing-library/react' import * as React from 'react' import { useLocale } from '@/context/i18n' import { LanguagesSupported } from '@/i18n-config/language' -import CSVDownload from './csv-downloader' +import CSVDownload from '../csv-downloader' const downloaderProps: any[] = [] diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.spec.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-uploader.spec.tsx similarity index 88% rename from web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.spec.tsx rename to web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-uploader.spec.tsx index 847db74619..d26ab051ef 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.spec.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-uploader.spec.tsx @@ -1,7 +1,7 @@ -import type { Props } from './csv-uploader' +import type { Props } from '../csv-uploader' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' -import CSVUploader from './csv-uploader' +import CSVUploader from '../csv-uploader' const toastMocks = vi.hoisted(() => ({ notify: vi.fn(), @@ -75,6 +75,20 @@ describe('CSVUploader', () => { expect(dropZone.className).not.toContain('border-components-dropzone-border-accent') }) + it('should handle drag over and clear dragging state when leaving through the overlay', () => { + renderComponent() + const { dropZone, dropContainer } = getDropElements() + + fireEvent.dragEnter(dropContainer) + const dragLayer = dropContainer.querySelector('.absolute') as HTMLDivElement + + fireEvent.dragOver(dropContainer) + fireEvent.dragLeave(dragLayer) + + expect(dropZone.className).not.toContain('border-components-dropzone-border-accent') + expect(dropZone.className).not.toContain('bg-components-dropzone-bg-accent') + }) + it('should ignore drop events without dataTransfer', () => { renderComponent() const { dropContainer } = getDropElements() diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/index.spec.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/index.spec.tsx similarity index 96% rename from web/app/components/app/annotation/batch-add-annotation-modal/index.spec.tsx rename to web/app/components/app/annotation/batch-add-annotation-modal/__tests__/index.spec.tsx index 8929cc292f..5104706045 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/index.spec.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/index.spec.tsx @@ -1,10 +1,10 @@ import type { Mock } from 'vitest' -import type { IBatchModalProps } from './index' +import type { IBatchModalProps } from '../index' import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' import { useProviderContext } from '@/context/provider-context' import { annotationBatchImport, checkAnnotationBatchImportProgress } from '@/service/annotation' -import BatchModal, { ProcessStatus } from './index' +import BatchModal, { ProcessStatus } from '../index' vi.mock('@/service/annotation', () => ({ annotationBatchImport: vi.fn(), @@ -15,13 +15,13 @@ vi.mock('@/context/provider-context', () => ({ useProviderContext: vi.fn(), })) -vi.mock('./csv-downloader', () => ({ +vi.mock('../csv-downloader', () => ({ default: () =>
, })) let lastUploadedFile: File | undefined -vi.mock('./csv-uploader', () => ({ +vi.mock('../csv-uploader', () => ({ default: ({ file, updateFile }: { file?: File, updateFile: (file?: File) => void }) => (
+ +
+ ) + }, +})) + +vi.mock('@/app/components/base/features/hooks', () => ({ + useFeatures: (selector: (state: { features: typeof mockFeatures }) => unknown) => selector({ features: mockFeatures }), + useFeaturesStore: () => ({ + getState: () => ({ + features: mockFeatures, + setFeatures: mockSetFeatures, + }), + }), +})) + +describe('FeaturesWrappedAppPublisher', () => { + const publishedConfig = { + modelConfig: { + more_like_this: { enabled: true }, + opening_statement: 'Hello there', + suggested_questions: ['Q1'], + sensitive_word_avoidance: { enabled: true }, + speech_to_text: { enabled: true }, + text_to_speech: { enabled: true }, + suggested_questions_after_answer: { enabled: true }, + retriever_resource: { enabled: true }, + annotation_reply: { enabled: true }, + file_upload: { + enabled: true, + image: { + enabled: true, + detail: 'low', + number_limits: 5, + transfer_methods: ['remote_url'], + }, + allowed_file_types: ['image'], + allowed_file_extensions: ['.jpg'], + allowed_file_upload_methods: ['remote_url'], + number_limits: 5, + }, + resetAppConfig: vi.fn(), + }, + } + + beforeEach(() => { + vi.clearAllMocks() + mockAppPublisherProps.current = null + }) + + it('should pass current features through to onPublish', async () => { + render( + , + ) + + fireEvent.click(screen.getByText('publish-through-wrapper')) + + await waitFor(() => { + expect(mockOnPublish).toHaveBeenCalledWith({ id: 'model-1' }, mockFeatures) + }) + }) + + it('should restore published features after confirmation', async () => { + render( + , + ) + + fireEvent.click(screen.getByText('restore-through-wrapper')) + fireEvent.click(screen.getByRole('button', { name: 'operation.confirm' })) + + await waitFor(() => { + expect(publishedConfig.modelConfig.resetAppConfig).toHaveBeenCalledTimes(1) + expect(mockSetFeatures).toHaveBeenCalledWith(expect.objectContaining({ + moreLikeThis: { enabled: true }, + opening: { + enabled: true, + opening_statement: 'Hello there', + suggested_questions: ['Q1'], + }, + moderation: { enabled: true }, + speech2text: { enabled: true }, + text2speech: { enabled: true }, + suggested: { enabled: true }, + citation: { enabled: true }, + annotationReply: { enabled: true }, + })) + }) + }) +}) diff --git a/web/app/components/app/app-publisher/__tests__/index.spec.tsx b/web/app/components/app/app-publisher/__tests__/index.spec.tsx new file mode 100644 index 0000000000..e97efaa525 --- /dev/null +++ b/web/app/components/app/app-publisher/__tests__/index.spec.tsx @@ -0,0 +1,455 @@ +/* eslint-disable ts/no-explicit-any */ +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import * as React from 'react' +import { AccessMode } from '@/models/access-control' +import { AppModeEnum } from '@/types/app' +import { basePath } from '@/utils/var' +import AppPublisher from '../index' + +const mockOnPublish = vi.fn() +const mockOnToggle = vi.fn() +const mockSetAppDetail = vi.fn() +const mockTrackEvent = vi.fn() +const mockRefetch = vi.fn() +const mockOpenAsyncWindow = vi.fn() +const mockFetchInstalledAppList = vi.fn() +const mockFetchAppDetailDirect = vi.fn() +const mockToastError = vi.fn() + +const sectionProps = vi.hoisted(() => ({ + summary: null as null | Record, + access: null as null | Record, + actions: null as null | Record, +})) +const ahooksMocks = vi.hoisted(() => ({ + keyPressHandlers: [] as Array<(event: { preventDefault: () => void }) => void>, +})) + +let mockAppDetail: Record | null = null + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('ahooks', async () => { + return { + useKeyPress: (_keys: unknown, handler: (event: { preventDefault: () => void }) => void) => { + ahooksMocks.keyPressHandlers.push(handler) + }, + } +}) + +vi.mock('@/app/components/app/store', () => ({ + useStore: (selector: (state: { appDetail: Record | null, setAppDetail: typeof mockSetAppDetail }) => unknown) => selector({ + appDetail: mockAppDetail, + setAppDetail: mockSetAppDetail, + }), +})) + +vi.mock('@/context/global-public-context', () => ({ + useGlobalPublicStore: (selector: (state: { systemFeatures: { webapp_auth: { enabled: boolean } } }) => unknown) => selector({ + systemFeatures: { + webapp_auth: { + enabled: true, + }, + }, + }), +})) + +vi.mock('@/hooks/use-format-time-from-now', () => ({ + useFormatTimeFromNow: () => ({ + formatTimeFromNow: () => 'moments ago', + }), +})) + +vi.mock('@/hooks/use-async-window-open', () => ({ + useAsyncWindowOpen: () => mockOpenAsyncWindow, +})) + +vi.mock('@/service/access-control', () => ({ + useGetUserCanAccessApp: () => ({ + data: { result: true }, + isLoading: false, + refetch: mockRefetch, + }), + useAppWhiteListSubjects: () => ({ + data: { groups: [], members: [] }, + isLoading: false, + }), +})) + +vi.mock('@/service/explore', () => ({ + fetchInstalledAppList: (...args: unknown[]) => mockFetchInstalledAppList(...args), +})) + +vi.mock('@/service/apps', () => ({ + fetchAppDetailDirect: (...args: unknown[]) => mockFetchAppDetailDirect(...args), +})) + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + error: (...args: unknown[]) => mockToastError(...args), + }, +})) + +vi.mock('@/app/components/base/amplitude', () => ({ + trackEvent: (...args: unknown[]) => mockTrackEvent(...args), +})) + +vi.mock('@/app/components/app/overview/embedded', () => ({ + default: ({ isShow, onClose }: { isShow: boolean, onClose: () => void }) => (isShow + ? ( +
+ embedded modal + +
+ ) + : null), +})) + +vi.mock('../../app-access-control', () => ({ + default: ({ onConfirm, onClose }: { onConfirm: () => Promise, onClose: () => void }) => ( +
+ + +
+ ), +})) + +vi.mock('@/app/components/base/portal-to-follow-elem', async () => { + const ReactModule = await vi.importActual('react') + const OpenContext = ReactModule.createContext(false) + + return { + PortalToFollowElem: ({ children, open }: { children: React.ReactNode, open: boolean }) => ( + +
{children}
+
+ ), + PortalToFollowElemTrigger: ({ children, onClick }: { children: React.ReactNode, onClick?: () => void }) => ( +
{children}
+ ), + PortalToFollowElemContent: ({ children }: { children: React.ReactNode }) => { + const open = ReactModule.useContext(OpenContext) + return open ?
{children}
: null + }, + } +}) + +vi.mock('../sections', () => ({ + PublisherSummarySection: (props: Record) => { + sectionProps.summary = props + return ( +
+ + +
+ ) + }, + PublisherAccessSection: (props: Record) => { + sectionProps.access = props + return + }, + PublisherActionsSection: (props: Record) => { + sectionProps.actions = props + return ( +
+ + +
+ ) + }, +})) + +describe('AppPublisher', () => { + beforeEach(() => { + vi.clearAllMocks() + ahooksMocks.keyPressHandlers.length = 0 + sectionProps.summary = null + sectionProps.access = null + sectionProps.actions = null + mockAppDetail = { + id: 'app-1', + name: 'Demo App', + mode: AppModeEnum.CHAT, + access_mode: AccessMode.SPECIFIC_GROUPS_MEMBERS, + site: { + app_base_url: 'https://example.com', + access_token: 'token-1', + }, + } + mockFetchInstalledAppList.mockResolvedValue({ + installed_apps: [{ id: 'installed-1' }], + }) + mockFetchAppDetailDirect.mockResolvedValue({ + id: 'app-1', + access_mode: AccessMode.PUBLIC, + }) + mockOpenAsyncWindow.mockImplementation(async (resolver: () => Promise) => { + await resolver() + }) + }) + + it('should open the publish popover and refetch access permission data', async () => { + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + + expect(screen.getByText('publisher-summary-publish')).toBeInTheDocument() + expect(mockOnToggle).toHaveBeenCalledWith(true) + + await waitFor(() => { + expect(mockRefetch).toHaveBeenCalledTimes(1) + }) + }) + + it('should publish and track the publish event', async () => { + mockOnPublish.mockResolvedValue(undefined) + + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-summary-publish')) + + await waitFor(() => { + expect(mockOnPublish).toHaveBeenCalledTimes(1) + expect(mockTrackEvent).toHaveBeenCalledWith('app_published_time', expect.objectContaining({ + action_mode: 'app', + app_id: 'app-1', + app_name: 'Demo App', + })) + }) + }) + + it('should open the embedded modal from the actions section', () => { + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-embed')) + + expect(screen.getByTestId('embedded-modal')).toBeInTheDocument() + }) + + it('should close embedded and access control panels through child callbacks', async () => { + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-embed')) + fireEvent.click(screen.getByText('close-embedded-modal')) + expect(screen.queryByTestId('embedded-modal')).not.toBeInTheDocument() + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-access-control')) + expect(screen.getByTestId('access-control')).toBeInTheDocument() + fireEvent.click(screen.getByText('close-access-control')) + expect(screen.queryByTestId('access-control')).not.toBeInTheDocument() + }) + + it('should refresh app detail after access control confirmation', async () => { + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-access-control')) + + expect(screen.getByTestId('access-control')).toBeInTheDocument() + + fireEvent.click(screen.getByText('confirm-access-control')) + + await waitFor(() => { + expect(mockFetchAppDetailDirect).toHaveBeenCalledWith({ url: '/apps', id: 'app-1' }) + expect(mockSetAppDetail).toHaveBeenCalledWith({ + id: 'app-1', + access_mode: AccessMode.PUBLIC, + }) + }) + }) + + it('should open the installed explore page through the async window helper', async () => { + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-open-in-explore')) + + await waitFor(() => { + expect(mockOpenAsyncWindow).toHaveBeenCalledTimes(1) + expect(mockFetchInstalledAppList).toHaveBeenCalledWith('app-1') + expect(sectionProps.actions?.appURL).toBe(`https://example.com${basePath}/chat/token-1`) + }) + }) + + it('should ignore the trigger when the publish button is disabled', () => { + render( + , + ) + + fireEvent.click(screen.getByText('common.publish').parentElement?.parentElement as HTMLElement) + + expect(screen.queryByText('publisher-summary-publish')).not.toBeInTheDocument() + expect(mockOnToggle).not.toHaveBeenCalled() + }) + + it('should publish from the keyboard shortcut and restore the popover state', async () => { + const preventDefault = vi.fn() + const onRestore = vi.fn().mockResolvedValue(undefined) + mockOnPublish.mockResolvedValue(undefined) + + render( + , + ) + + ahooksMocks.keyPressHandlers[0]({ preventDefault }) + + await waitFor(() => { + expect(preventDefault).toHaveBeenCalled() + expect(mockOnPublish).toHaveBeenCalledTimes(1) + }) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-summary-restore')) + + await waitFor(() => { + expect(onRestore).toHaveBeenCalledTimes(1) + }) + expect(screen.queryByText('publisher-summary-publish')).not.toBeInTheDocument() + }) + + it('should keep the popover open when restore fails and reset published state after publish failures', async () => { + const preventDefault = vi.fn() + const onRestore = vi.fn().mockRejectedValue(new Error('restore failed')) + mockOnPublish.mockRejectedValueOnce(new Error('publish failed')) + + render( + , + ) + + ahooksMocks.keyPressHandlers[0]({ preventDefault }) + + await waitFor(() => { + expect(preventDefault).toHaveBeenCalled() + expect(mockOnPublish).toHaveBeenCalledTimes(1) + }) + expect(mockTrackEvent).not.toHaveBeenCalled() + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-summary-restore')) + + await waitFor(() => { + expect(onRestore).toHaveBeenCalledTimes(1) + }) + expect(screen.getByText('publisher-summary-publish')).toBeInTheDocument() + }) + + it('should report missing explore installations', async () => { + mockFetchInstalledAppList.mockResolvedValueOnce({ + installed_apps: [], + }) + mockOpenAsyncWindow.mockImplementation(async (resolver: () => Promise, options: { onError: (error: Error) => void }) => { + try { + await resolver() + } + catch (error) { + options.onError(error as Error) + } + }) + + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-open-in-explore')) + + await waitFor(() => { + expect(mockToastError).toHaveBeenCalledWith('No app found in Explore') + }) + }) + + it('should report explore errors when the app cannot be opened', async () => { + mockAppDetail = { + ...mockAppDetail, + id: undefined, + } + mockOpenAsyncWindow.mockImplementation(async (resolver: () => Promise, options: { onError: (error: Error) => void }) => { + try { + await resolver() + } + catch (error) { + options.onError(error as Error) + } + }) + + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-open-in-explore')) + + await waitFor(() => { + expect(mockToastError).toHaveBeenCalledWith('App not found') + }) + }) + + it('should keep access control open when app detail is unavailable during confirmation', async () => { + mockAppDetail = null + + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-access-control')) + fireEvent.click(screen.getByText('confirm-access-control')) + + await waitFor(() => { + expect(mockFetchAppDetailDirect).not.toHaveBeenCalled() + }) + expect(screen.getByTestId('access-control')).toBeInTheDocument() + }) +}) diff --git a/web/app/components/app/app-publisher/__tests__/publish-with-multiple-model.spec.tsx b/web/app/components/app/app-publisher/__tests__/publish-with-multiple-model.spec.tsx new file mode 100644 index 0000000000..f476d8b188 --- /dev/null +++ b/web/app/components/app/app-publisher/__tests__/publish-with-multiple-model.spec.tsx @@ -0,0 +1,110 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import * as React from 'react' +import PublishWithMultipleModel from '../publish-with-multiple-model' + +const mockUseProviderContext = vi.fn() + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('@/context/provider-context', () => ({ + useProviderContext: () => mockUseProviderContext(), +})) + +vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({ + useLanguage: () => 'en_US', +})) + +vi.mock('../../header/account-setting/model-provider-page/model-icon', () => ({ + default: ({ modelName }: { modelName: string }) => {modelName}, +})) + +vi.mock('@/app/components/base/portal-to-follow-elem', async () => { + const ReactModule = await vi.importActual('react') + const OpenContext = ReactModule.createContext(false) + + return { + PortalToFollowElem: ({ children, open }: { children: React.ReactNode, open: boolean }) => ( + +
{children}
+
+ ), + PortalToFollowElemTrigger: ({ children, onClick, className }: { children: React.ReactNode, onClick?: () => void, className?: string }) => ( +
+ {children} +
+ ), + PortalToFollowElemContent: ({ children, className }: { children: React.ReactNode, className?: string }) => { + const open = ReactModule.useContext(OpenContext) + return open ?
{children}
: null + }, + } +}) + +describe('PublishWithMultipleModel', () => { + beforeEach(() => { + vi.clearAllMocks() + mockUseProviderContext.mockReturnValue({ + textGenerationModelList: [ + { + provider: 'openai', + models: [ + { + model: 'gpt-4o', + label: { + en_US: 'GPT-4o', + }, + }, + ], + }, + ], + }) + }) + + it('should disable the trigger when no valid model configuration is available', () => { + render( + , + ) + + expect(screen.getByRole('button', { name: 'operation.applyConfig' })).toBeDisabled() + expect(screen.queryByText('publishAs')).not.toBeInTheDocument() + }) + + it('should open matching model options and call onSelect', () => { + const handleSelect = vi.fn() + const modelConfig = { + id: 'config-1', + provider: 'openai', + model: 'gpt-4o', + parameters: { temperature: 0.7 }, + } + + render( + , + ) + + fireEvent.click(screen.getByRole('button', { name: 'operation.applyConfig' })) + + expect(screen.getByText('publishAs')).toBeInTheDocument() + + fireEvent.click(screen.getByText('GPT-4o')) + + expect(handleSelect).toHaveBeenCalledWith(expect.objectContaining(modelConfig)) + }) +}) diff --git a/web/app/components/app/app-publisher/__tests__/sections.spec.tsx b/web/app/components/app/app-publisher/__tests__/sections.spec.tsx new file mode 100644 index 0000000000..57e7a55b13 --- /dev/null +++ b/web/app/components/app/app-publisher/__tests__/sections.spec.tsx @@ -0,0 +1,266 @@ +/* eslint-disable ts/no-explicit-any */ +import type { ReactNode } from 'react' +import { fireEvent, render, screen } from '@testing-library/react' +import { AccessMode } from '@/models/access-control' +import { AppModeEnum } from '@/types/app' +import { AccessModeDisplay, PublisherAccessSection, PublisherActionsSection, PublisherSummarySection } from '../sections' + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('../publish-with-multiple-model', () => ({ + default: ({ onSelect }: { onSelect: (item: Record) => void }) => ( + + ), +})) + +vi.mock('../suggested-action', () => ({ + default: ({ children, onClick, link, disabled }: { children: ReactNode, onClick?: () => void, link?: string, disabled?: boolean }) => ( + + ), +})) + +vi.mock('@/app/components/tools/workflow-tool/configure-button', () => ({ + default: (props: Record) => ( +
+ workflow-tool-configure + {String(props.disabledReason || '')} +
+ ), +})) + +describe('app-publisher sections', () => { + it('should render restore controls for published chat apps', () => { + const handleRestore = vi.fn() + + render( + '3 minutes ago'} + handlePublish={vi.fn()} + handleRestore={handleRestore} + isChatApp + multipleModelConfigs={[]} + publishDisabled={false} + published={false} + publishedAt={Date.now()} + publishShortcut={['ctrl', '⇧', 'P']} + startNodeLimitExceeded={false} + upgradeHighlightStyle={{}} + />, + ) + + fireEvent.click(screen.getByText('common.restore')) + expect(handleRestore).toHaveBeenCalled() + }) + + it('should expose the access control warning when subjects are missing', () => { + render( + , + ) + + expect(screen.getByText('publishApp.notSet')).toBeInTheDocument() + expect(screen.getByText('publishApp.notSetDesc')).toBeInTheDocument() + }) + + it('should render the publish update action when the draft has not been published yet', () => { + render( + '1 minute ago'} + handlePublish={vi.fn()} + handleRestore={vi.fn()} + isChatApp={false} + multipleModelConfigs={[]} + publishDisabled={false} + published={false} + publishedAt={undefined} + publishShortcut={['ctrl', '⇧', 'P']} + startNodeLimitExceeded={false} + upgradeHighlightStyle={{}} + />, + ) + + expect(screen.getByText('common.publishUpdate')).toBeInTheDocument() + }) + + it('should render multiple-model publishing', () => { + const handlePublish = vi.fn() + + render( + '1 minute ago'} + handlePublish={handlePublish} + handleRestore={vi.fn()} + isChatApp={false} + multipleModelConfigs={[{ id: '1' } as any]} + publishDisabled={false} + published={false} + publishedAt={undefined} + publishShortcut={['ctrl', '⇧', 'P']} + startNodeLimitExceeded={false} + upgradeHighlightStyle={{}} + />, + ) + + fireEvent.click(screen.getByText('publish-multiple-model')) + + expect(handlePublish).toHaveBeenCalledWith({ model: 'gpt-4o' }) + }) + + it('should render the upgrade hint when the start node limit is exceeded', () => { + render( + '1 minute ago'} + handlePublish={vi.fn()} + handleRestore={vi.fn()} + isChatApp={false} + multipleModelConfigs={[]} + publishDisabled={false} + published={false} + publishedAt={undefined} + publishShortcut={['ctrl', '⇧', 'P']} + startNodeLimitExceeded + upgradeHighlightStyle={{}} + />, + ) + + expect(screen.getByText('publishLimit.startNodeDesc')).toBeInTheDocument() + }) + + it('should render loading access state and access mode labels when enabled', () => { + const { rerender } = render( + , + ) + + expect(document.querySelector('.spin-animation')).toBeInTheDocument() + + rerender( + , + ) + + expect(screen.getByText('accessControlDialog.accessItems.anyone')).toBeInTheDocument() + expect(render().container).toBeEmptyDOMElement() + }) + + it('should render workflow actions, batch run links, and workflow tool configuration', () => { + const handleOpenInExplore = vi.fn() + const handleEmbed = vi.fn() + + const { rerender } = render( + , + ) + + expect(screen.getByText('common.batchRunApp')).toHaveAttribute('data-link', 'https://example.com/app?mode=batch') + fireEvent.click(screen.getByText('common.openInExplore')) + expect(handleOpenInExplore).toHaveBeenCalled() + expect(screen.getByText('workflow-tool-configure')).toBeInTheDocument() + expect(screen.getByText('workflow-disabled')).toBeInTheDocument() + + rerender( + , + ) + + fireEvent.click(screen.getByText('common.embedIntoSite')) + expect(handleEmbed).toHaveBeenCalled() + expect(screen.getByText('common.accessAPIReference')).toBeDisabled() + + rerender( + , + ) + + expect(screen.queryByText('common.runApp')).not.toBeInTheDocument() + }) +}) diff --git a/web/app/components/app/app-publisher/__tests__/suggested-action.spec.tsx b/web/app/components/app/app-publisher/__tests__/suggested-action.spec.tsx new file mode 100644 index 0000000000..ea199dfb78 --- /dev/null +++ b/web/app/components/app/app-publisher/__tests__/suggested-action.spec.tsx @@ -0,0 +1,49 @@ +import type { MouseEvent as ReactMouseEvent } from 'react' +import { fireEvent, render, screen } from '@testing-library/react' +import SuggestedAction from '../suggested-action' + +describe('SuggestedAction', () => { + it('should render an enabled external link', () => { + render( + + Open docs + , + ) + + const link = screen.getByRole('link', { name: 'Open docs' }) + expect(link).toHaveAttribute('href', 'https://example.com/docs') + expect(link).toHaveAttribute('target', '_blank') + }) + + it('should block clicks when disabled', () => { + const handleClick = vi.fn() + + render( + + Disabled action + , + ) + + const link = screen.getByText('Disabled action').closest('a') as HTMLAnchorElement + fireEvent.click(link) + + expect(link).not.toHaveAttribute('href') + expect(handleClick).not.toHaveBeenCalled() + }) + + it('should forward click events when enabled', () => { + const handleClick = vi.fn((event: ReactMouseEvent) => { + event.preventDefault() + }) + + render( + + Enabled action + , + ) + + fireEvent.click(screen.getByRole('link', { name: 'Enabled action' })) + + expect(handleClick).toHaveBeenCalledTimes(1) + }) +}) diff --git a/web/app/components/app/app-publisher/__tests__/utils.spec.ts b/web/app/components/app/app-publisher/__tests__/utils.spec.ts new file mode 100644 index 0000000000..9f191ce514 --- /dev/null +++ b/web/app/components/app/app-publisher/__tests__/utils.spec.ts @@ -0,0 +1,70 @@ +import type { TFunction } from 'i18next' +import { AccessMode } from '@/models/access-control' +import { AppModeEnum } from '@/types/app' +import { basePath } from '@/utils/var' +import { + getDisabledFunctionTooltip, + getPublisherAppMode, + getPublisherAppUrl, + isPublisherAccessConfigured, +} from '../utils' + +describe('app-publisher utils', () => { + describe('getPublisherAppMode', () => { + it('should normalize chat-like apps to chat mode', () => { + expect(getPublisherAppMode(AppModeEnum.AGENT_CHAT)).toBe(AppModeEnum.CHAT) + }) + + it('should keep completion mode unchanged', () => { + expect(getPublisherAppMode(AppModeEnum.COMPLETION)).toBe(AppModeEnum.COMPLETION) + }) + }) + + describe('getPublisherAppUrl', () => { + it('should build the published app url from site info', () => { + expect(getPublisherAppUrl({ + appBaseUrl: 'https://example.com', + accessToken: 'token-1', + mode: AppModeEnum.CHAT, + })).toBe(`https://example.com${basePath}/chat/token-1`) + }) + }) + + describe('isPublisherAccessConfigured', () => { + it('should require members or groups for specific access mode', () => { + expect(isPublisherAccessConfigured( + { access_mode: AccessMode.SPECIFIC_GROUPS_MEMBERS }, + { groups: [], members: [] }, + )).toBe(false) + }) + + it('should treat public access as configured', () => { + expect(isPublisherAccessConfigured( + { access_mode: AccessMode.PUBLIC }, + { groups: [], members: [] }, + )).toBe(true) + }) + }) + + describe('getDisabledFunctionTooltip', () => { + const t = ((key: string) => key) as unknown as TFunction + + it('should prioritize the unpublished hint', () => { + expect(getDisabledFunctionTooltip({ + t, + publishedAt: undefined, + missingStartNode: false, + noAccessPermission: false, + })).toBe('notPublishedYet') + }) + + it('should return the access error when the app is published but blocked', () => { + expect(getDisabledFunctionTooltip({ + t, + publishedAt: Date.now(), + missingStartNode: false, + noAccessPermission: true, + })).toBe('noAccessPermission') + }) + }) +}) diff --git a/web/app/components/app/app-publisher/__tests__/version-info-modal.spec.tsx b/web/app/components/app/app-publisher/__tests__/version-info-modal.spec.tsx new file mode 100644 index 0000000000..fa8b1c92fc --- /dev/null +++ b/web/app/components/app/app-publisher/__tests__/version-info-modal.spec.tsx @@ -0,0 +1,128 @@ +/* eslint-disable ts/no-explicit-any */ +import { fireEvent, render, screen } from '@testing-library/react' +import { toast } from '@/app/components/base/ui/toast' +import VersionInfoModal from '../version-info-modal' + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + error: vi.fn(), + }, +})) + +describe('VersionInfoModal', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should prefill the fields from the current version info', () => { + render( + , + ) + + expect(screen.getByDisplayValue('Release 1')).toBeInTheDocument() + expect(screen.getByDisplayValue('Initial release')).toBeInTheDocument() + }) + + it('should reject overlong titles', () => { + const handlePublish = vi.fn() + + render( + , + ) + + const [titleInput] = screen.getAllByRole('textbox') + fireEvent.change(titleInput, { target: { value: 'a'.repeat(16) } }) + fireEvent.click(screen.getByRole('button', { name: 'common.publish' })) + + expect(toast.error).toHaveBeenCalledWith('versionHistory.editField.titleLengthLimit') + expect(handlePublish).not.toHaveBeenCalled() + }) + + it('should publish valid values and close the modal', () => { + const handlePublish = vi.fn() + const handleClose = vi.fn() + + render( + , + ) + + const [titleInput, notesInput] = screen.getAllByRole('textbox') + fireEvent.change(titleInput, { target: { value: 'Release 2' } }) + fireEvent.change(notesInput, { target: { value: 'Updated notes' } }) + fireEvent.click(screen.getByRole('button', { name: 'common.publish' })) + + expect(handlePublish).toHaveBeenCalledWith({ + title: 'Release 2', + releaseNotes: 'Updated notes', + id: 'version-2', + }) + expect(handleClose).toHaveBeenCalledTimes(1) + }) + + it('should validate release note length and clear previous errors before publishing', () => { + const handlePublish = vi.fn() + const handleClose = vi.fn() + + render( + , + ) + + const [titleInput, notesInput] = screen.getAllByRole('textbox') + + fireEvent.change(titleInput, { target: { value: 'a'.repeat(16) } }) + fireEvent.click(screen.getByRole('button', { name: 'common.publish' })) + expect(toast.error).toHaveBeenCalledWith('versionHistory.editField.titleLengthLimit') + + fireEvent.change(titleInput, { target: { value: 'Release 3' } }) + fireEvent.change(notesInput, { target: { value: 'b'.repeat(101) } }) + fireEvent.click(screen.getByRole('button', { name: 'common.publish' })) + expect(toast.error).toHaveBeenCalledWith('versionHistory.editField.releaseNotesLengthLimit') + + fireEvent.change(notesInput, { target: { value: 'Stable release notes' } }) + fireEvent.click(screen.getByRole('button', { name: 'common.publish' })) + + expect(handlePublish).toHaveBeenCalledWith({ + title: 'Release 3', + releaseNotes: 'Stable release notes', + id: 'version-3', + }) + expect(handleClose).toHaveBeenCalledTimes(1) + }) +}) diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index 0b5f629829..8792a1c507 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -1,6 +1,5 @@ import type { ModelAndParameter } from '../configuration/debug/types' import type { InputVar, Variable } from '@/app/components/workflow/types' -import type { I18nKeysByPrefix } from '@/types/i18n' import type { PublishWorkflowParams } from '@/types/workflow' import { useKeyPress } from 'ahooks' import { @@ -15,15 +14,11 @@ import EmbeddedModal from '@/app/components/app/overview/embedded' import { useStore as useAppStore } from '@/app/components/app/store' import { trackEvent } from '@/app/components/base/amplitude' import Button from '@/app/components/base/button' -import { CodeBrowser } from '@/app/components/base/icons/src/vender/line/development' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' -import UpgradeBtn from '@/app/components/billing/upgrade-btn' -import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button' -import { appDefaultIconBackground } from '@/config' import { useGlobalPublicStore } from '@/context/global-public-context' import { useAsyncWindowOpen } from '@/hooks/use-async-window-open' import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' @@ -33,54 +28,19 @@ import { fetchAppDetailDirect } from '@/service/apps' import { fetchInstalledAppList } from '@/service/explore' import { AppModeEnum } from '@/types/app' import { basePath } from '@/utils/var' -import Divider from '../../base/divider' -import Loading from '../../base/loading' -import Tooltip from '../../base/tooltip' import { toast } from '../../base/ui/toast' -import ShortcutsName from '../../workflow/shortcuts-name' import { getKeyboardKeyCodeBySystem } from '../../workflow/utils' import AccessControl from '../app-access-control' -import PublishWithMultipleModel from './publish-with-multiple-model' -import SuggestedAction from './suggested-action' - -type AccessModeLabel = I18nKeysByPrefix<'app', 'accessControlDialog.accessItems.'> - -const ACCESS_MODE_MAP: Record = { - [AccessMode.ORGANIZATION]: { - label: 'organization', - icon: 'i-ri-building-line', - }, - [AccessMode.SPECIFIC_GROUPS_MEMBERS]: { - label: 'specific', - icon: 'i-ri-lock-line', - }, - [AccessMode.PUBLIC]: { - label: 'anyone', - icon: 'i-ri-global-line', - }, - [AccessMode.EXTERNAL_MEMBERS]: { - label: 'external', - icon: 'i-ri-verified-badge-line', - }, -} - -const AccessModeDisplay: React.FC<{ mode?: AccessMode }> = ({ mode }) => { - const { t } = useTranslation() - - if (!mode || !ACCESS_MODE_MAP[mode]) - return null - - const { icon, label } = ACCESS_MODE_MAP[mode] - - return ( - <> - -
- {t(`accessControlDialog.accessItems.${label}`, { ns: 'app' })} -
- - ) -} +import { + PublisherAccessSection, + PublisherActionsSection, + PublisherSummarySection, +} from './sections' +import { + getDisabledFunctionTooltip, + getPublisherAppUrl, + isPublisherAccessConfigured, +} from './utils' export type AppPublisherProps = { disabled?: boolean @@ -143,32 +103,28 @@ const AppPublisher = ({ const { formatTimeFromNow } = useFormatTimeFromNow() const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {} - const appMode = (appDetail?.mode !== AppModeEnum.COMPLETION && appDetail?.mode !== AppModeEnum.WORKFLOW) ? AppModeEnum.CHAT : appDetail.mode - const appURL = `${appBaseURL}${basePath}/${appMode}/${accessToken}` + const appURL = getPublisherAppUrl({ appBaseUrl: appBaseURL, accessToken, mode: appDetail?.mode }) const isChatApp = [AppModeEnum.CHAT, AppModeEnum.AGENT_CHAT, AppModeEnum.COMPLETION].includes(appDetail?.mode || AppModeEnum.CHAT) const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp, refetch } = useGetUserCanAccessApp({ appId: appDetail?.id, enabled: false }) const { data: appAccessSubjects, isLoading: isGettingAppWhiteListSubjects } = useAppWhiteListSubjects(appDetail?.id, open && systemFeatures.webapp_auth.enabled && appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS) const openAsyncWindow = useAsyncWindowOpen() - const isAppAccessSet = useMemo(() => { - if (appDetail && appAccessSubjects) { - return !(appDetail.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && appAccessSubjects.groups?.length === 0 && appAccessSubjects.members?.length === 0) - } - return true - }, [appAccessSubjects, appDetail]) + const isAppAccessSet = useMemo(() => isPublisherAccessConfigured(appDetail, appAccessSubjects), [appAccessSubjects, appDetail]) - const noAccessPermission = useMemo(() => systemFeatures.webapp_auth.enabled && appDetail && appDetail.access_mode !== AccessMode.EXTERNAL_MEMBERS && !userCanAccessApp?.result, [systemFeatures, appDetail, userCanAccessApp]) + const noAccessPermission = useMemo(() => Boolean( + systemFeatures.webapp_auth.enabled + && appDetail + && appDetail.access_mode !== AccessMode.EXTERNAL_MEMBERS + && !userCanAccessApp?.result, + ), [systemFeatures, appDetail, userCanAccessApp]) const disabledFunctionButton = useMemo(() => (!publishedAt || missingStartNode || noAccessPermission), [publishedAt, missingStartNode, noAccessPermission]) - - const disabledFunctionTooltip = useMemo(() => { - if (!publishedAt) - return t('notPublishedYet', { ns: 'app' }) - if (missingStartNode) - return t('noUserInputNode', { ns: 'app' }) - if (noAccessPermission) - return t('noAccessPermission', { ns: 'app' }) - }, [missingStartNode, noAccessPermission, publishedAt, t]) + const disabledFunctionTooltip = useMemo(() => getDisabledFunctionTooltip({ + t, + publishedAt, + missingStartNode, + noAccessPermission, + }), [missingStartNode, noAccessPermission, publishedAt, t]) useEffect(() => { if (systemFeatures.webapp_auth.enabled && open && appDetail) @@ -244,9 +200,9 @@ const AppPublisher = ({ }, { exactMatch: true, useCapture: true }) const hasPublishedVersion = !!publishedAt - const workflowToolDisabled = !hasPublishedVersion || !workflowToolAvailable - const workflowToolMessage = workflowToolDisabled ? t('common.workflowAsToolDisabledHint', { ns: 'workflow' }) : undefined - const showStartNodeLimitHint = Boolean(startNodeLimitExceeded) + const workflowToolMessage = !hasPublishedVersion || !workflowToolAvailable + ? t('common.workflowAsToolDisabledHint', { ns: 'workflow' }) + : undefined const upgradeHighlightStyle = useMemo(() => ({ background: 'linear-gradient(97deg, var(--components-input-border-active-prompt-1, rgba(11, 165, 236, 0.95)) -3.64%, var(--components-input-border-active-prompt-2, rgba(21, 90, 239, 0.95)) 45.14%)', WebkitBackgroundClip: 'text', @@ -277,199 +233,51 @@ const AppPublisher = ({
-
-
- {publishedAt ? t('common.latestPublished', { ns: 'workflow' }) : t('common.currentDraftUnpublished', { ns: 'workflow' })} -
- {publishedAt - ? ( -
-
- {t('common.publishedAt', { ns: 'workflow' })} - {' '} - {formatTimeFromNow(publishedAt)} -
- {isChatApp && ( - - )} -
- ) - : ( -
- {t('common.autoSaved', { ns: 'workflow' })} - {' '} - · - {Boolean(draftUpdatedAt) && formatTimeFromNow(draftUpdatedAt!)} -
- )} - {debugWithMultipleModel - ? ( - handlePublish(item)} - // textGenerationModelList={textGenerationModelList} - /> - ) - : ( - <> - - {showStartNodeLimitHint && ( -
-

- {t('publishLimit.startNodeTitlePrefix', { ns: 'workflow' })} - {t('publishLimit.startNodeTitleSuffix', { ns: 'workflow' })} -

-

- {t('publishLimit.startNodeDesc', { ns: 'workflow' })} -

- -
- )} - - )} -
- {(systemFeatures.webapp_auth.enabled && (isGettingUserCanAccessApp || isGettingAppWhiteListSubjects)) - ?
- : ( - <> - - {systemFeatures.webapp_auth.enabled && ( -
-
-

{t('publishApp.title', { ns: 'app' })}

-
-
{ - setShowAppAccessControl(true) - }} - > -
- -
- {!isAppAccessSet &&

{t('publishApp.notSet', { ns: 'app' })}

} -
- -
-
- {!isAppAccessSet &&

{t('publishApp.notSetDesc', { ns: 'app' })}

} -
- )} - { - // Hide run/batch run app buttons when there is a trigger node. - !hasTriggerNode && ( -
- - } - > - {t('common.runApp', { ns: 'workflow' })} - - - {appDetail?.mode === AppModeEnum.WORKFLOW || appDetail?.mode === AppModeEnum.COMPLETION - ? ( - - } - > - {t('common.batchRunApp', { ns: 'workflow' })} - - - ) - : ( - { - setEmbeddingModalOpen(true) - handleTrigger() - }} - disabled={!publishedAt} - icon={} - > - {t('common.embedIntoSite', { ns: 'workflow' })} - - )} - - { - if (publishedAt) - handleOpenInExplore() - }} - disabled={disabledFunctionButton} - icon={} - > - {t('common.openInExplore', { ns: 'workflow' })} - - - - } - > - {t('common.accessAPIReference', { ns: 'workflow' })} - - - {appDetail?.mode === AppModeEnum.WORKFLOW && !hasHumanInputNode && ( - - )} -
- ) - } - - )} + + setShowAppAccessControl(true)} + /> + { + setEmbeddingModalOpen(true) + handleTrigger() + }} + handleOpenInExplore={handleOpenInExplore} + handlePublish={handlePublish} + hasHumanInputNode={hasHumanInputNode} + hasTriggerNode={hasTriggerNode} + inputs={inputs} + missingStartNode={missingStartNode} + onRefreshData={onRefreshData} + outputs={outputs} + published={published} + publishedAt={publishedAt} + toolPublished={toolPublished} + workflowToolAvailable={workflowToolAvailable} + workflowToolMessage={workflowToolMessage} + />
& { + formatTimeFromNow: (value: number) => string + handlePublish: (params?: ModelAndParameter | PublishWorkflowParams) => Promise + handleRestore: () => Promise + isChatApp: boolean + published: boolean + publishShortcut: string[] + upgradeHighlightStyle: CSSProperties + } + +type AccessSectionProps = { + enabled: boolean + isAppAccessSet: boolean + isLoading: boolean + accessMode?: keyof typeof ACCESS_MODE_MAP + onClick: () => void +} + +type ActionsSectionProps = Pick & { + appDetail: { + id?: string + icon?: string + icon_type?: string | null + icon_background?: string | null + description?: string + mode?: AppModeEnum + name?: string + } | null | undefined + appURL: string + disabledFunctionButton: boolean + disabledFunctionTooltip?: string + handleEmbed: () => void + handleOpenInExplore: () => void + handlePublish: (params?: ModelAndParameter | PublishWorkflowParams) => Promise + published: boolean + workflowToolMessage?: string + } + +export const AccessModeDisplay = ({ mode }: { mode?: keyof typeof ACCESS_MODE_MAP }) => { + const { t } = useTranslation() + + if (!mode || !ACCESS_MODE_MAP[mode]) + return null + + const { icon, label } = ACCESS_MODE_MAP[mode] + + return ( + <> + +
+ {t(`accessControlDialog.accessItems.${label}`, { ns: 'app' })} +
+ + ) +} + +export const PublisherSummarySection = ({ + debugWithMultipleModel = false, + draftUpdatedAt, + formatTimeFromNow, + handlePublish, + handleRestore, + isChatApp, + multipleModelConfigs = [], + publishDisabled = false, + published, + publishedAt, + publishShortcut, + startNodeLimitExceeded = false, + upgradeHighlightStyle, +}: SummarySectionProps) => { + const { t } = useTranslation() + + return ( +
+
+ {publishedAt ? t('common.latestPublished', { ns: 'workflow' }) : t('common.currentDraftUnpublished', { ns: 'workflow' })} +
+ {publishedAt + ? ( +
+
+ {t('common.publishedAt', { ns: 'workflow' })} + {' '} + {formatTimeFromNow(publishedAt)} +
+ {isChatApp && ( + + )} +
+ ) + : ( +
+ {t('common.autoSaved', { ns: 'workflow' })} + {' '} + · + {Boolean(draftUpdatedAt) && formatTimeFromNow(draftUpdatedAt!)} +
+ )} + {debugWithMultipleModel + ? ( + handlePublish(item)} + /> + ) + : ( + <> + + {startNodeLimitExceeded && ( +
+

+ {t('publishLimit.startNodeTitlePrefix', { ns: 'workflow' })} + {t('publishLimit.startNodeTitleSuffix', { ns: 'workflow' })} +

+

+ {t('publishLimit.startNodeDesc', { ns: 'workflow' })} +

+ +
+ )} + + )} +
+ ) +} + +export const PublisherAccessSection = ({ + enabled, + isAppAccessSet, + isLoading, + accessMode, + onClick, +}: AccessSectionProps) => { + const { t } = useTranslation() + + if (isLoading) + return
+ + return ( + <> + + {enabled && ( +
+
+

{t('publishApp.title', { ns: 'app' })}

+
+
+
+ +
+ {!isAppAccessSet &&

{t('publishApp.notSet', { ns: 'app' })}

} +
+ +
+
+ {!isAppAccessSet &&

{t('publishApp.notSetDesc', { ns: 'app' })}

} +
+ )} + + ) +} + +const ActionTooltip = ({ + disabled, + tooltip, + children, +}: { + disabled: boolean + tooltip?: ReactNode + children: ReactNode +}) => { + if (!disabled || !tooltip) + return <>{children} + + return ( + + {children}
} /> + + {tooltip} + + + ) +} + +export const PublisherActionsSection = ({ + appDetail, + appURL, + disabledFunctionButton, + disabledFunctionTooltip, + handleEmbed, + handleOpenInExplore, + handlePublish, + hasHumanInputNode = false, + hasTriggerNode = false, + inputs, + missingStartNode = false, + onRefreshData, + outputs, + published, + publishedAt, + toolPublished, + workflowToolAvailable = true, + workflowToolMessage, +}: ActionsSectionProps) => { + const { t } = useTranslation() + + if (hasTriggerNode) + return null + + const workflowToolDisabled = !publishedAt || !workflowToolAvailable + + return ( +
+ + } + > + {t('common.runApp', { ns: 'workflow' })} + + + {appDetail?.mode === AppModeEnum.WORKFLOW || appDetail?.mode === AppModeEnum.COMPLETION + ? ( + + } + > + {t('common.batchRunApp', { ns: 'workflow' })} + + + ) + : ( + } + > + {t('common.embedIntoSite', { ns: 'workflow' })} + + )} + + { + if (publishedAt) + handleOpenInExplore() + }} + disabled={disabledFunctionButton} + icon={} + > + {t('common.openInExplore', { ns: 'workflow' })} + + + + } + > + {t('common.accessAPIReference', { ns: 'workflow' })} + + + {appDetail?.mode === AppModeEnum.WORKFLOW && !hasHumanInputNode && ( + + )} +
+ ) +} diff --git a/web/app/components/app/app-publisher/suggested-action.tsx b/web/app/components/app/app-publisher/suggested-action.tsx index 71ddceec01..56879d8fea 100644 --- a/web/app/components/app/app-publisher/suggested-action.tsx +++ b/web/app/components/app/app-publisher/suggested-action.tsx @@ -2,7 +2,7 @@ import type { HTMLProps, PropsWithChildren } from 'react' import { RiArrowRightUpLine } from '@remixicon/react' import { cn } from '@/utils/classnames' -export type SuggestedActionProps = PropsWithChildren & { +type SuggestedActionProps = PropsWithChildren & { icon?: React.ReactNode link?: string disabled?: boolean diff --git a/web/app/components/app/app-publisher/utils.ts b/web/app/components/app/app-publisher/utils.ts new file mode 100644 index 0000000000..5ce8a4425d --- /dev/null +++ b/web/app/components/app/app-publisher/utils.ts @@ -0,0 +1,84 @@ +import type { TFunction } from 'i18next' +import type { I18nKeysByPrefix } from '@/types/i18n' +import { AccessMode } from '@/models/access-control' +import { AppModeEnum } from '@/types/app' +import { basePath } from '@/utils/var' + +type AccessSubjectsLike = { + groups?: unknown[] + members?: unknown[] +} | null | undefined + +type AppDetailLike = { + access_mode?: AccessMode + mode?: AppModeEnum +} + +type AccessModeLabel = I18nKeysByPrefix<'app', 'accessControlDialog.accessItems.'> + +export const ACCESS_MODE_MAP: Record = { + [AccessMode.ORGANIZATION]: { + label: 'organization', + icon: 'i-ri-building-line', + }, + [AccessMode.SPECIFIC_GROUPS_MEMBERS]: { + label: 'specific', + icon: 'i-ri-lock-line', + }, + [AccessMode.PUBLIC]: { + label: 'anyone', + icon: 'i-ri-global-line', + }, + [AccessMode.EXTERNAL_MEMBERS]: { + label: 'external', + icon: 'i-ri-verified-badge-line', + }, +} + +export const getPublisherAppMode = (mode?: AppModeEnum) => { + if (mode !== AppModeEnum.COMPLETION && mode !== AppModeEnum.WORKFLOW) + return AppModeEnum.CHAT + + return mode +} + +export const getPublisherAppUrl = ({ + appBaseUrl, + accessToken, + mode, +}: { + appBaseUrl: string + accessToken: string + mode?: AppModeEnum +}) => `${appBaseUrl}${basePath}/${getPublisherAppMode(mode)}/${accessToken}` + +export const isPublisherAccessConfigured = (appDetail: AppDetailLike | null | undefined, appAccessSubjects: AccessSubjectsLike) => { + if (!appDetail || !appAccessSubjects) + return true + + if (appDetail.access_mode !== AccessMode.SPECIFIC_GROUPS_MEMBERS) + return true + + return Boolean(appAccessSubjects.groups?.length || appAccessSubjects.members?.length) +} + +export const getDisabledFunctionTooltip = ({ + t, + publishedAt, + missingStartNode, + noAccessPermission, +}: { + t: TFunction + publishedAt?: number + missingStartNode: boolean + noAccessPermission: boolean +}) => { + if (!publishedAt) + return t('notPublishedYet', { ns: 'app' }) + if (missingStartNode) + return t('noUserInputNode', { ns: 'app' }) + if (noAccessPermission) + return t('noAccessPermission', { ns: 'app' }) + + return undefined +} diff --git a/web/app/components/app/configuration/__tests__/configuration-view.spec.tsx b/web/app/components/app/configuration/__tests__/configuration-view.spec.tsx new file mode 100644 index 0000000000..459df5d063 --- /dev/null +++ b/web/app/components/app/configuration/__tests__/configuration-view.spec.tsx @@ -0,0 +1,283 @@ +import type { ComponentProps } from 'react' +import type { ConfigurationViewModel } from '../hooks/use-configuration' +import type AppPublisher from '@/app/components/app/app-publisher/features-wrapper' +import type ConfigContext from '@/context/debug-configuration' +import { fireEvent, render, screen } from '@testing-library/react' +import * as React from 'react' +import { AppModeEnum, ModelModeType } from '@/types/app' +import ConfigurationView from '../configuration-view' + +vi.mock('@/app/components/app/app-publisher/features-wrapper', () => ({ + default: () =>
, +})) + +vi.mock('@/app/components/app/configuration/config', () => ({ + default: () =>
, +})) + +vi.mock('@/app/components/app/configuration/debug', () => ({ + default: () =>
, +})) + +vi.mock('@/app/components/app/configuration/config/agent-setting-button', () => ({ + default: () =>
, +})) + +vi.mock('@/app/components/header/account-setting/model-provider-page/model-parameter-modal', () => ({ + default: () =>
, +})) + +vi.mock('@/app/components/app/configuration/dataset-config/select-dataset', () => ({ + default: () =>
, +})) + +vi.mock('@/app/components/app/configuration/config-prompt/conversation-history/edit-modal', () => ({ + default: () =>
, +})) + +vi.mock('@/app/components/base/features/new-feature-panel', () => ({ + default: () =>
, +})) + +vi.mock('@/app/components/workflow/plugin-dependency', () => ({ + default: () =>
, +})) + +const createContextValue = (): ComponentProps['value'] => ({ + appId: 'app-1', + isAPIKeySet: true, + isTrailFinished: false, + mode: AppModeEnum.CHAT, + modelModeType: ModelModeType.chat, + promptMode: 'simple' as never, + setPromptMode: vi.fn(), + isAdvancedMode: false, + isAgent: false, + isFunctionCall: false, + isOpenAI: false, + collectionList: [], + canReturnToSimpleMode: false, + setCanReturnToSimpleMode: vi.fn(), + chatPromptConfig: { prompt: [] } as never, + completionPromptConfig: { + prompt: { text: '' }, + conversation_histories_role: { + user_prefix: 'user', + assistant_prefix: 'assistant', + }, + } as never, + currentAdvancedPrompt: [], + setCurrentAdvancedPrompt: vi.fn(), + showHistoryModal: vi.fn(), + conversationHistoriesRole: { + user_prefix: 'user', + assistant_prefix: 'assistant', + }, + setConversationHistoriesRole: vi.fn(), + hasSetBlockStatus: { + context: false, + history: true, + query: true, + }, + conversationId: '', + setConversationId: vi.fn(), + introduction: '', + setIntroduction: vi.fn(), + suggestedQuestions: [], + setSuggestedQuestions: vi.fn(), + controlClearChatMessage: 0, + setControlClearChatMessage: vi.fn(), + prevPromptConfig: { + prompt_template: '', + prompt_variables: [], + }, + setPrevPromptConfig: vi.fn(), + moreLikeThisConfig: { enabled: false }, + setMoreLikeThisConfig: vi.fn(), + suggestedQuestionsAfterAnswerConfig: { enabled: false }, + setSuggestedQuestionsAfterAnswerConfig: vi.fn(), + speechToTextConfig: { enabled: false }, + setSpeechToTextConfig: vi.fn(), + textToSpeechConfig: { enabled: false, voice: '', language: '' }, + setTextToSpeechConfig: vi.fn(), + citationConfig: { enabled: false }, + setCitationConfig: vi.fn(), + annotationConfig: { + id: '', + enabled: false, + score_threshold: 0.5, + embedding_model: { + embedding_model_name: '', + embedding_provider_name: '', + }, + }, + setAnnotationConfig: vi.fn(), + moderationConfig: { enabled: false }, + setModerationConfig: vi.fn(), + externalDataToolsConfig: [], + setExternalDataToolsConfig: vi.fn(), + formattingChanged: false, + setFormattingChanged: vi.fn(), + inputs: {}, + setInputs: vi.fn(), + query: '', + setQuery: vi.fn(), + completionParams: {}, + setCompletionParams: vi.fn(), + modelConfig: { + provider: 'openai', + model_id: 'gpt-4o', + mode: ModelModeType.chat, + configs: { + prompt_template: '', + prompt_variables: [], + }, + chat_prompt_config: null, + completion_prompt_config: null, + opening_statement: '', + more_like_this: null, + suggested_questions: [], + suggested_questions_after_answer: null, + speech_to_text: null, + text_to_speech: null, + file_upload: null, + retriever_resource: null, + sensitive_word_avoidance: null, + annotation_reply: null, + external_data_tools: [], + system_parameters: { + audio_file_size_limit: 1, + file_size_limit: 1, + image_file_size_limit: 1, + video_file_size_limit: 1, + workflow_file_upload_limit: 1, + }, + dataSets: [], + agentConfig: { + enabled: false, + strategy: 'react', + max_iteration: 1, + tools: [], + }, + } as never, + setModelConfig: vi.fn(), + dataSets: [], + setDataSets: vi.fn(), + showSelectDataSet: vi.fn(), + datasetConfigs: { + retrieval_model: 'multiple', + reranking_model: { + reranking_provider_name: '', + reranking_model_name: '', + }, + top_k: 3, + score_threshold_enabled: false, + score_threshold: 0.5, + datasets: { datasets: [] }, + } as never, + datasetConfigsRef: { current: {} as never }, + setDatasetConfigs: vi.fn(), + hasSetContextVar: false, + isShowVisionConfig: false, + visionConfig: { + enabled: false, + number_limits: 1, + detail: 'low', + transfer_methods: ['local_file'], + } as never, + setVisionConfig: vi.fn(), + isAllowVideoUpload: false, + isShowDocumentConfig: false, + isShowAudioConfig: false, + rerankSettingModalOpen: false, + setRerankSettingModalOpen: vi.fn(), +}) + +const createViewModel = (overrides: Partial = {}): ConfigurationViewModel => ({ + appPublisherProps: { + publishDisabled: false, + publishedAt: 0, + debugWithMultipleModel: false, + multipleModelConfigs: [], + onPublish: vi.fn(), + publishedConfig: { + modelConfig: createContextValue().modelConfig, + completionParams: {}, + }, + resetAppConfig: vi.fn(), + } as ComponentProps, + contextValue: createContextValue(), + featuresData: { + moreLikeThis: { enabled: false }, + opening: { enabled: false, opening_statement: '', suggested_questions: [] }, + moderation: { enabled: false }, + speech2text: { enabled: false }, + text2speech: { enabled: false, voice: '', language: '' }, + file: { enabled: false, image: { enabled: false, detail: 'high', number_limits: 3, transfer_methods: ['local_file'] } } as never, + suggested: { enabled: false }, + citation: { enabled: false }, + annotationReply: { enabled: false }, + }, + isAgent: false, + isAdvancedMode: false, + isMobile: false, + isShowDebugPanel: false, + isShowHistoryModal: false, + isShowSelectDataSet: false, + modelConfig: createContextValue().modelConfig, + multipleModelConfigs: [], + onAutoAddPromptVariable: vi.fn(), + onAgentSettingChange: vi.fn(), + onCloseFeaturePanel: vi.fn(), + onCloseHistoryModal: vi.fn(), + onCloseSelectDataSet: vi.fn(), + onCompletionParamsChange: vi.fn(), + onConfirmUseGPT4: vi.fn(), + onEnableMultipleModelDebug: vi.fn(), + onFeaturesChange: vi.fn(), + onHideDebugPanel: vi.fn(), + onModelChange: vi.fn(), + onMultipleModelConfigsChange: vi.fn(), + onOpenAccountSettings: vi.fn(), + onOpenDebugPanel: vi.fn(), + onSaveHistory: vi.fn(), + onSelectDataSets: vi.fn(), + promptVariables: [], + selectedIds: [], + showAppConfigureFeaturesModal: false, + showLoading: false, + showUseGPT4Confirm: false, + setShowUseGPT4Confirm: vi.fn(), + ...overrides, +}) + +describe('ConfigurationView', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should render a loading state before configuration data is ready', () => { + render() + + expect(screen.getByRole('status', { name: 'appApi.loading' })).toBeInTheDocument() + expect(screen.queryByTestId('app-publisher')).not.toBeInTheDocument() + }) + + it('should open the mobile debug panel from the header button', () => { + const onOpenDebugPanel = vi.fn() + render() + + fireEvent.click(screen.getByRole('button', { name: /appDebug.operation.debugConfig/i })) + + expect(onOpenDebugPanel).toHaveBeenCalledTimes(1) + }) + + it('should close the GPT-4 confirmation dialog when cancel is clicked', () => { + const setShowUseGPT4Confirm = vi.fn() + render() + + fireEvent.click(screen.getByRole('button', { name: /operation.cancel/i })) + + expect(setShowUseGPT4Confirm).toHaveBeenCalledWith(false) + }) +}) diff --git a/web/app/components/app/configuration/__tests__/index.spec.tsx b/web/app/components/app/configuration/__tests__/index.spec.tsx new file mode 100644 index 0000000000..b855725b1d --- /dev/null +++ b/web/app/components/app/configuration/__tests__/index.spec.tsx @@ -0,0 +1,32 @@ +import { render } from '@testing-library/react' +import * as React from 'react' +import { useConfiguration } from '../hooks/use-configuration' +import Configuration from '../index' + +const mockView = vi.fn((_: unknown) =>
) + +vi.mock('../configuration-view', () => ({ + default: (props: unknown) => mockView(props), +})) + +vi.mock('../hooks/use-configuration', () => ({ + useConfiguration: vi.fn(), +})) + +describe('Configuration entry', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should pass the hook view model into ConfigurationView', () => { + const viewModel = { + showLoading: true, + } + vi.mocked(useConfiguration).mockReturnValue(viewModel as never) + + render() + + expect(useConfiguration).toHaveBeenCalledTimes(1) + expect(mockView).toHaveBeenCalledWith(viewModel) + }) +}) diff --git a/web/app/components/app/configuration/__tests__/utils.spec.ts b/web/app/components/app/configuration/__tests__/utils.spec.ts new file mode 100644 index 0000000000..65a6192177 --- /dev/null +++ b/web/app/components/app/configuration/__tests__/utils.spec.ts @@ -0,0 +1,226 @@ +import type { ModelConfig } from '@/models/debug' +import { AppModeEnum, ModelModeType, Resolution, TransferMethod } from '@/types/app' +import { buildConfigurationFeaturesData, getConfigurationPublishingState, withCollectionIconBasePath } from '../utils' + +const createModelConfig = (overrides: Partial = {}): ModelConfig => ({ + provider: 'openai', + model_id: 'gpt-4o', + mode: ModelModeType.chat, + configs: { + prompt_template: 'Hello', + prompt_variables: [], + }, + chat_prompt_config: { + prompt: [], + } as ModelConfig['chat_prompt_config'], + completion_prompt_config: { + prompt: { text: '' }, + conversation_histories_role: { + user_prefix: 'user', + assistant_prefix: 'assistant', + }, + } as ModelConfig['completion_prompt_config'], + opening_statement: '', + more_like_this: { enabled: false }, + suggested_questions: [], + suggested_questions_after_answer: { enabled: false }, + speech_to_text: { enabled: false }, + text_to_speech: { enabled: false, voice: '', language: '' }, + file_upload: null, + retriever_resource: { enabled: false }, + sensitive_word_avoidance: { enabled: false }, + annotation_reply: null, + external_data_tools: [], + system_parameters: { + audio_file_size_limit: 1, + file_size_limit: 1, + image_file_size_limit: 1, + video_file_size_limit: 1, + workflow_file_upload_limit: 1, + }, + dataSets: [], + agentConfig: { + enabled: false, + strategy: 'react', + max_iteration: 1, + tools: [], + } as ModelConfig['agentConfig'], + ...overrides, +}) + +describe('configuration utils', () => { + describe('withCollectionIconBasePath', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should prefix relative collection icons with the base path', () => { + const result = withCollectionIconBasePath([ + { id: 'tool-1', icon: '/icons/tool.svg' }, + { id: 'tool-2', icon: '/console/icons/prefixed.svg' }, + ] as never, '/console') + + expect(result[0].icon).toBe('/console/icons/tool.svg') + expect(result[1].icon).toBe('/console/icons/prefixed.svg') + }) + }) + + describe('buildConfigurationFeaturesData', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should derive feature toggles and upload fallbacks from model config', () => { + const result = buildConfigurationFeaturesData(createModelConfig({ + opening_statement: 'Welcome', + suggested_questions: ['How are you?'], + file_upload: { + enabled: true, + image: { + enabled: true, + detail: Resolution.low, + number_limits: 2, + transfer_methods: [TransferMethod.local_file], + }, + allowed_file_types: ['image'], + allowed_file_extensions: ['.png'], + allowed_file_upload_methods: [TransferMethod.local_file], + number_limits: 2, + }, + }), undefined) + + expect(result.opening).toEqual({ + enabled: true, + opening_statement: 'Welcome', + suggested_questions: ['How are you?'], + }) + expect(result.file).toBeDefined() + expect(result.file!.enabled).toBe(true) + expect(result.file!.image!.detail).toBe(Resolution.low) + expect(result.file!.allowed_file_extensions).toEqual(['.png']) + }) + }) + + describe('getConfigurationPublishingState', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should block publish when advanced completion mode is missing required blocks', () => { + const result = getConfigurationPublishingState({ + chatPromptConfig: { + prompt: [], + } as never, + completionPromptConfig: { + prompt: { text: 'Answer' }, + conversation_histories_role: { + user_prefix: 'user', + assistant_prefix: 'assistant', + }, + } as never, + hasSetBlockStatus: { + context: false, + history: false, + query: false, + }, + hasSetContextVar: false, + hasSelectedDataSets: false, + isAdvancedMode: true, + mode: AppModeEnum.CHAT, + modelModeType: ModelModeType.completion, + promptTemplate: 'ignored', + }) + + expect(result.promptEmpty).toBe(false) + expect(result.cannotPublish).toBe(true) + }) + + it('should require a context variable only for completion apps with selected datasets', () => { + const result = getConfigurationPublishingState({ + chatPromptConfig: { + prompt: [], + } as never, + completionPromptConfig: { + prompt: { text: 'Completion prompt' }, + conversation_histories_role: { + user_prefix: 'user', + assistant_prefix: 'assistant', + }, + } as never, + hasSetBlockStatus: { + context: false, + history: true, + query: true, + }, + hasSetContextVar: false, + hasSelectedDataSets: true, + isAdvancedMode: false, + mode: AppModeEnum.COMPLETION, + modelModeType: ModelModeType.completion, + promptTemplate: 'Prompt', + }) + + expect(result.promptEmpty).toBe(false) + expect(result.cannotPublish).toBe(false) + expect(result.contextVarEmpty).toBe(true) + }) + + it('should treat advanced completion chat prompts as empty when every segment is blank', () => { + const result = getConfigurationPublishingState({ + chatPromptConfig: { + prompt: [{ text: '' }, { text: '' }], + } as never, + completionPromptConfig: { + prompt: { text: 'ignored' }, + conversation_histories_role: { + user_prefix: 'user', + assistant_prefix: 'assistant', + }, + } as never, + hasSetBlockStatus: { + context: true, + history: true, + query: true, + }, + hasSetContextVar: true, + hasSelectedDataSets: false, + isAdvancedMode: true, + mode: AppModeEnum.COMPLETION, + modelModeType: ModelModeType.chat, + promptTemplate: 'ignored', + }) + + expect(result.promptEmpty).toBe(true) + expect(result.cannotPublish).toBe(true) + }) + + it('should treat advanced completion text prompts as empty when the completion prompt is missing', () => { + const result = getConfigurationPublishingState({ + chatPromptConfig: { + prompt: [{ text: 'ignored' }], + } as never, + completionPromptConfig: { + prompt: { text: '' }, + conversation_histories_role: { + user_prefix: 'user', + assistant_prefix: 'assistant', + }, + } as never, + hasSetBlockStatus: { + context: true, + history: true, + query: true, + }, + hasSetContextVar: true, + hasSelectedDataSets: false, + isAdvancedMode: true, + mode: AppModeEnum.COMPLETION, + modelModeType: ModelModeType.completion, + promptTemplate: 'ignored', + }) + + expect(result.promptEmpty).toBe(true) + expect(result.cannotPublish).toBe(true) + }) + }) +}) diff --git a/web/app/components/app/configuration/base/feature-panel/index.spec.tsx b/web/app/components/app/configuration/base/feature-panel/__tests__/index.spec.tsx similarity index 98% rename from web/app/components/app/configuration/base/feature-panel/index.spec.tsx rename to web/app/components/app/configuration/base/feature-panel/__tests__/index.spec.tsx index 7e1b661399..0daf638b49 100644 --- a/web/app/components/app/configuration/base/feature-panel/index.spec.tsx +++ b/web/app/components/app/configuration/base/feature-panel/__tests__/index.spec.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react' -import FeaturePanel from './index' +import FeaturePanel from '../index' describe('FeaturePanel', () => { // Rendering behavior for standard layout. diff --git a/web/app/components/app/configuration/base/feature-panel/index.tsx b/web/app/components/app/configuration/base/feature-panel/index.tsx index 7f337c1572..77ee7bc8dd 100644 --- a/web/app/components/app/configuration/base/feature-panel/index.tsx +++ b/web/app/components/app/configuration/base/feature-panel/index.tsx @@ -3,7 +3,7 @@ import type { FC, ReactNode } from 'react' import * as React from 'react' import { cn } from '@/utils/classnames' -export type IFeaturePanelProps = { +type IFeaturePanelProps = { className?: string headerIcon?: ReactNode title: ReactNode diff --git a/web/app/components/app/configuration/base/group-name/index.spec.tsx b/web/app/components/app/configuration/base/group-name/__tests__/index.spec.tsx similarity index 92% rename from web/app/components/app/configuration/base/group-name/index.spec.tsx rename to web/app/components/app/configuration/base/group-name/__tests__/index.spec.tsx index be698c3233..ce1ee7a18a 100644 --- a/web/app/components/app/configuration/base/group-name/index.spec.tsx +++ b/web/app/components/app/configuration/base/group-name/__tests__/index.spec.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react' -import GroupName from './index' +import GroupName from '../index' describe('GroupName', () => { beforeEach(() => { diff --git a/web/app/components/app/configuration/base/group-name/index.tsx b/web/app/components/app/configuration/base/group-name/index.tsx index b21b0c5825..210ba4ca87 100644 --- a/web/app/components/app/configuration/base/group-name/index.tsx +++ b/web/app/components/app/configuration/base/group-name/index.tsx @@ -2,7 +2,7 @@ import type { FC } from 'react' import * as React from 'react' -export type IGroupNameProps = { +type IGroupNameProps = { name: string } diff --git a/web/app/components/app/configuration/base/operation-btn/index.spec.tsx b/web/app/components/app/configuration/base/operation-btn/__tests__/index.spec.tsx similarity index 98% rename from web/app/components/app/configuration/base/operation-btn/index.spec.tsx rename to web/app/components/app/configuration/base/operation-btn/__tests__/index.spec.tsx index 8e254d261b..325aebe6c0 100644 --- a/web/app/components/app/configuration/base/operation-btn/index.spec.tsx +++ b/web/app/components/app/configuration/base/operation-btn/__tests__/index.spec.tsx @@ -1,5 +1,5 @@ import { fireEvent, render, screen } from '@testing-library/react' -import OperationBtn from './index' +import OperationBtn from '../index' vi.mock('@remixicon/react', () => ({ RiAddLine: (props: { className?: string }) => ( diff --git a/web/app/components/app/configuration/base/operation-btn/index.tsx b/web/app/components/app/configuration/base/operation-btn/index.tsx index d33b632071..e3bdfd01ba 100644 --- a/web/app/components/app/configuration/base/operation-btn/index.tsx +++ b/web/app/components/app/configuration/base/operation-btn/index.tsx @@ -9,7 +9,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { cn } from '@/utils/classnames' -export type IOperationBtnProps = { +type IOperationBtnProps = { className?: string type: 'add' | 'edit' actionName?: string diff --git a/web/app/components/app/configuration/base/var-highlight/index.spec.tsx b/web/app/components/app/configuration/base/var-highlight/__tests__/index.spec.tsx similarity index 97% rename from web/app/components/app/configuration/base/var-highlight/index.spec.tsx rename to web/app/components/app/configuration/base/var-highlight/__tests__/index.spec.tsx index 77fe1f2b28..1add8601c4 100644 --- a/web/app/components/app/configuration/base/var-highlight/index.spec.tsx +++ b/web/app/components/app/configuration/base/var-highlight/__tests__/index.spec.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react' -import VarHighlight, { varHighlightHTML } from './index' +import VarHighlight, { varHighlightHTML } from '../index' describe('VarHighlight', () => { beforeEach(() => { diff --git a/web/app/components/app/configuration/base/var-highlight/index.tsx b/web/app/components/app/configuration/base/var-highlight/index.tsx index 697007d0b0..fda58f0b65 100644 --- a/web/app/components/app/configuration/base/var-highlight/index.tsx +++ b/web/app/components/app/configuration/base/var-highlight/index.tsx @@ -4,7 +4,7 @@ import * as React from 'react' import s from './style.module.css' -export type IVarHighlightProps = { +type IVarHighlightProps = { name: string className?: string } diff --git a/web/app/components/app/configuration/base/var-highlight/style.module.css b/web/app/components/app/configuration/base/var-highlight/style.module.css index b7a66085ce..2bcef0dabb 100644 --- a/web/app/components/app/configuration/base/var-highlight/style.module.css +++ b/web/app/components/app/configuration/base/var-highlight/style.module.css @@ -1,5 +1,3 @@ -@reference "../../../../../styles/globals.css"; - .item { background-color: rgba(21, 94, 239, 0.05); } diff --git a/web/app/components/app/configuration/base/warning-mask/cannot-query-dataset.spec.tsx b/web/app/components/app/configuration/base/warning-mask/__tests__/cannot-query-dataset.spec.tsx similarity index 94% rename from web/app/components/app/configuration/base/warning-mask/cannot-query-dataset.spec.tsx rename to web/app/components/app/configuration/base/warning-mask/__tests__/cannot-query-dataset.spec.tsx index 730b251e67..161bd5073d 100644 --- a/web/app/components/app/configuration/base/warning-mask/cannot-query-dataset.spec.tsx +++ b/web/app/components/app/configuration/base/warning-mask/__tests__/cannot-query-dataset.spec.tsx @@ -1,6 +1,6 @@ import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' -import CannotQueryDataset from './cannot-query-dataset' +import CannotQueryDataset from '../cannot-query-dataset' describe('CannotQueryDataset WarningMask', () => { it('should render dataset warning copy and action button', () => { diff --git a/web/app/components/app/configuration/base/warning-mask/formatting-changed.spec.tsx b/web/app/components/app/configuration/base/warning-mask/__tests__/formatting-changed.spec.tsx similarity index 95% rename from web/app/components/app/configuration/base/warning-mask/formatting-changed.spec.tsx rename to web/app/components/app/configuration/base/warning-mask/__tests__/formatting-changed.spec.tsx index 9b5a5d93e1..81655f2d99 100644 --- a/web/app/components/app/configuration/base/warning-mask/formatting-changed.spec.tsx +++ b/web/app/components/app/configuration/base/warning-mask/__tests__/formatting-changed.spec.tsx @@ -1,6 +1,6 @@ import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' -import FormattingChanged from './formatting-changed' +import FormattingChanged from '../formatting-changed' describe('FormattingChanged WarningMask', () => { it('should display translation text and both actions', () => { diff --git a/web/app/components/app/configuration/base/warning-mask/has-not-set-api.spec.tsx b/web/app/components/app/configuration/base/warning-mask/__tests__/has-not-set-api.spec.tsx similarity index 93% rename from web/app/components/app/configuration/base/warning-mask/has-not-set-api.spec.tsx rename to web/app/components/app/configuration/base/warning-mask/__tests__/has-not-set-api.spec.tsx index abcf5795d0..5995f3472f 100644 --- a/web/app/components/app/configuration/base/warning-mask/has-not-set-api.spec.tsx +++ b/web/app/components/app/configuration/base/warning-mask/__tests__/has-not-set-api.spec.tsx @@ -1,6 +1,6 @@ import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' -import HasNotSetAPI from './has-not-set-api' +import HasNotSetAPI from '../has-not-set-api' describe('HasNotSetAPI', () => { it('should render the empty state copy', () => { diff --git a/web/app/components/app/configuration/base/warning-mask/index.spec.tsx b/web/app/components/app/configuration/base/warning-mask/__tests__/index.spec.tsx similarity index 95% rename from web/app/components/app/configuration/base/warning-mask/index.spec.tsx rename to web/app/components/app/configuration/base/warning-mask/__tests__/index.spec.tsx index cb8ef0b678..d287a790e6 100644 --- a/web/app/components/app/configuration/base/warning-mask/index.spec.tsx +++ b/web/app/components/app/configuration/base/warning-mask/__tests__/index.spec.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react' import * as React from 'react' -import WarningMask from './index' +import WarningMask from '../index' describe('WarningMask', () => { // Rendering of title, description, and footer content diff --git a/web/app/components/app/configuration/base/warning-mask/cannot-query-dataset.tsx b/web/app/components/app/configuration/base/warning-mask/cannot-query-dataset.tsx index 791230566b..d5fbd9e78f 100644 --- a/web/app/components/app/configuration/base/warning-mask/cannot-query-dataset.tsx +++ b/web/app/components/app/configuration/base/warning-mask/cannot-query-dataset.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import WarningMask from '.' -export type IFormattingChangedProps = { +type IFormattingChangedProps = { onConfirm: () => void } diff --git a/web/app/components/app/configuration/base/warning-mask/formatting-changed.tsx b/web/app/components/app/configuration/base/warning-mask/formatting-changed.tsx index 1fe7b9c182..56ccae5ade 100644 --- a/web/app/components/app/configuration/base/warning-mask/formatting-changed.tsx +++ b/web/app/components/app/configuration/base/warning-mask/formatting-changed.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import WarningMask from '.' -export type IFormattingChangedProps = { +type IFormattingChangedProps = { onConfirm: () => void onCancel: () => void } diff --git a/web/app/components/app/configuration/base/warning-mask/has-not-set-api.tsx b/web/app/components/app/configuration/base/warning-mask/has-not-set-api.tsx index 06c81a9f95..25587c5e58 100644 --- a/web/app/components/app/configuration/base/warning-mask/has-not-set-api.tsx +++ b/web/app/components/app/configuration/base/warning-mask/has-not-set-api.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react' import * as React from 'react' import { useTranslation } from 'react-i18next' -export type IHasNotSetAPIProps = { +type IHasNotSetAPIProps = { onSetting: () => void } diff --git a/web/app/components/app/configuration/base/warning-mask/index.tsx b/web/app/components/app/configuration/base/warning-mask/index.tsx index 6d6aeceb97..5275f022cb 100644 --- a/web/app/components/app/configuration/base/warning-mask/index.tsx +++ b/web/app/components/app/configuration/base/warning-mask/index.tsx @@ -4,7 +4,7 @@ import * as React from 'react' import s from './style.module.css' -export type IWarningMaskProps = { +type IWarningMaskProps = { title: string description: string footer: React.ReactNode diff --git a/web/app/components/app/configuration/base/warning-mask/style.module.css b/web/app/components/app/configuration/base/warning-mask/style.module.css index f922ec332f..a2c394de2a 100644 --- a/web/app/components/app/configuration/base/warning-mask/style.module.css +++ b/web/app/components/app/configuration/base/warning-mask/style.module.css @@ -1,5 +1,3 @@ -@reference "../../../../../styles/globals.css"; - .mask { backdrop-filter: blur(2px); } diff --git a/web/app/components/app/configuration/config-prompt/__tests__/advanced-prompt-input.spec.tsx b/web/app/components/app/configuration/config-prompt/__tests__/advanced-prompt-input.spec.tsx new file mode 100644 index 0000000000..413721ee2e --- /dev/null +++ b/web/app/components/app/configuration/config-prompt/__tests__/advanced-prompt-input.spec.tsx @@ -0,0 +1,228 @@ +/* eslint-disable ts/no-explicit-any */ +import type { ReactNode } from 'react' +import type { PromptRole } from '@/models/debug' +import { fireEvent, render, screen } from '@testing-library/react' +import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '@/app/components/base/prompt-editor/plugins/variable-block' +import ConfigContext from '@/context/debug-configuration' +import { AppModeEnum } from '@/types/app' +import AdvancedPromptInput from '../advanced-prompt-input' + +const mockEmit = vi.fn() +const mockSetShowExternalDataToolModal = vi.fn() +const mockSetModelConfig = vi.fn() +const mockOnTypeChange = vi.fn() +const mockOnChange = vi.fn() +const mockOnDelete = vi.fn() +const mockOnHideContextMissingTip = vi.fn() +const mockCopy = vi.fn() +const mockToastError = vi.fn() + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('copy-to-clipboard', () => ({ + default: (...args: unknown[]) => mockCopy(...args), +})) + +vi.mock('@remixicon/react', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + RiDeleteBinLine: ({ onClick }: { onClick: () => void }) => ( + + ), + RiErrorWarningFill: () => warning-icon, + } +}) + +vi.mock('@/app/components/base/icons/src/vender/line/files', () => ({ + Copy: ({ onClick }: { onClick: () => void }) => ( + + ), + CopyCheck: () => copy-checked, +})) + +vi.mock('@/context/event-emitter', () => ({ + useEventEmitterContextContext: () => ({ + eventEmitter: { + emit: (...args: unknown[]) => mockEmit(...args), + }, + }), +})) + +vi.mock('@/context/modal-context', () => ({ + useModalContext: () => ({ + setShowExternalDataToolModal: mockSetShowExternalDataToolModal, + }), +})) + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + error: (...args: unknown[]) => mockToastError(...args), + }, +})) + +vi.mock('../message-type-selector', () => ({ + default: ({ onChange, value }: { onChange: (value: PromptRole) => void, value: PromptRole }) => ( + + ), +})) + +vi.mock('@/app/components/base/prompt-editor', () => ({ + default: (props: { + onBlur: () => void + onChange: (value: string) => void + externalToolBlock: { onAddExternalTool: () => void } + }) => ( +
+ + + +
+ ), +})) + +vi.mock('../prompt-editor-height-resize-wrap', () => ({ + default: ({ children, footer }: { children: ReactNode, footer: ReactNode }) => ( +
+ {children} + {footer} +
+ ), +})) + +const createContextValue = () => ({ + mode: AppModeEnum.CHAT, + hasSetBlockStatus: { + context: false, + history: false, + query: false, + }, + modelConfig: { + configs: { + prompt_variables: [ + { key: 'existing_var', name: 'Existing', type: 'string', required: true }, + ], + }, + }, + setModelConfig: mockSetModelConfig, + conversationHistoriesRole: { + user_prefix: 'user', + assistant_prefix: 'assistant', + }, + showHistoryModal: vi.fn(), + dataSets: [], + showSelectDataSet: vi.fn(), + externalDataToolsConfig: [], +}) as any + +describe('AdvancedPromptInput', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should delegate prompt text and role changes to the parent callbacks', () => { + render( + + + , + ) + + fireEvent.click(screen.getByText('change-advanced')) + fireEvent.click(screen.getByText('selector:user')) + fireEvent.click(screen.getByText('copy-prompt')) + fireEvent.click(screen.getByText('delete-prompt')) + + expect(mockOnChange).toHaveBeenCalledWith('Updated {{new_var}}') + expect(mockOnTypeChange).toHaveBeenCalledWith('assistant') + expect(mockCopy).toHaveBeenCalledWith('Hello') + expect(mockOnDelete).toHaveBeenCalled() + }) + + it('should add newly discovered variables after blur confirmation', () => { + render( + + + , + ) + + fireEvent.click(screen.getByText('blur-advanced')) + fireEvent.click(screen.getByText('operation.add')) + + expect(mockSetModelConfig).toHaveBeenCalledWith(expect.objectContaining({ + configs: expect.objectContaining({ + prompt_variables: expect.arrayContaining([ + expect.objectContaining({ + key: 'new_var', + name: 'new_var', + }), + ]), + }), + })) + }) + + it('should open the external data tool modal and validate duplicates', () => { + render( + + + , + ) + + fireEvent.click(screen.getByText('open-advanced-tool-modal')) + + const modalConfig = mockSetShowExternalDataToolModal.mock.calls[0][0] + expect(modalConfig.onValidateBeforeSaveCallback({ variable: 'existing_var' })).toBe(false) + expect(mockToastError).toHaveBeenCalledWith('varKeyError.keyAlreadyExists') + + modalConfig.onSaveCallback({ + label: 'Search', + variable: 'search_api', + }) + + expect(mockEmit).toHaveBeenCalledWith(expect.objectContaining({ + type: 'ADD_EXTERNAL_DATA_TOOL', + })) + expect(mockEmit).toHaveBeenCalledWith(expect.objectContaining({ + payload: 'search_api', + type: INSERT_VARIABLE_VALUE_BLOCK_COMMAND, + })) + }) +}) diff --git a/web/app/components/app/configuration/config-prompt/index.spec.tsx b/web/app/components/app/configuration/config-prompt/__tests__/index.spec.tsx similarity index 98% rename from web/app/components/app/configuration/config-prompt/index.spec.tsx rename to web/app/components/app/configuration/config-prompt/__tests__/index.spec.tsx index c784a09ab6..d42eedf16b 100644 --- a/web/app/components/app/configuration/config-prompt/index.spec.tsx +++ b/web/app/components/app/configuration/config-prompt/__tests__/index.spec.tsx @@ -1,4 +1,5 @@ -import type { IPromptProps } from './index' +/* eslint-disable ts/no-explicit-any */ +import type { IPromptProps } from '../index' import type { PromptItem, PromptVariable } from '@/models/debug' import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' @@ -6,7 +7,7 @@ import { MAX_PROMPT_MESSAGE_LENGTH } from '@/config' import ConfigContext from '@/context/debug-configuration' import { PromptRole } from '@/models/debug' import { AppModeEnum, ModelModeType } from '@/types/app' -import Prompt from './index' +import Prompt from '../index' type DebugConfiguration = { isAdvancedMode: boolean @@ -30,7 +31,7 @@ const defaultPromptVariables: PromptVariable[] = [ let mockSimplePromptInputProps: IPromptProps | null = null -vi.mock('./simple-prompt-input', () => ({ +vi.mock('../simple-prompt-input', () => ({ default: (props: IPromptProps) => { mockSimplePromptInputProps = props return ( @@ -65,7 +66,7 @@ type AdvancedMessageInputProps = { noResize?: boolean } -vi.mock('./advanced-prompt-input', () => ({ +vi.mock('../advanced-prompt-input', () => ({ default: (props: AdvancedMessageInputProps) => { return (
{ beforeEach(() => { diff --git a/web/app/components/app/configuration/config-prompt/prompt-editor-height-resize-wrap.spec.tsx b/web/app/components/app/configuration/config-prompt/__tests__/prompt-editor-height-resize-wrap.spec.tsx similarity index 95% rename from web/app/components/app/configuration/config-prompt/prompt-editor-height-resize-wrap.spec.tsx rename to web/app/components/app/configuration/config-prompt/__tests__/prompt-editor-height-resize-wrap.spec.tsx index abd95e7660..7e168cc7f7 100644 --- a/web/app/components/app/configuration/config-prompt/prompt-editor-height-resize-wrap.spec.tsx +++ b/web/app/components/app/configuration/config-prompt/__tests__/prompt-editor-height-resize-wrap.spec.tsx @@ -1,6 +1,6 @@ import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' -import PromptEditorHeightResizeWrap from './prompt-editor-height-resize-wrap' +import PromptEditorHeightResizeWrap from '../prompt-editor-height-resize-wrap' describe('PromptEditorHeightResizeWrap', () => { beforeEach(() => { diff --git a/web/app/components/app/configuration/config-prompt/__tests__/simple-prompt-input.spec.tsx b/web/app/components/app/configuration/config-prompt/__tests__/simple-prompt-input.spec.tsx new file mode 100644 index 0000000000..a0bc072760 --- /dev/null +++ b/web/app/components/app/configuration/config-prompt/__tests__/simple-prompt-input.spec.tsx @@ -0,0 +1,320 @@ +/* eslint-disable ts/no-explicit-any */ +import type { ReactNode } from 'react' +import { fireEvent, render, screen } from '@testing-library/react' +import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '@/app/components/base/prompt-editor/plugins/variable-block' +import ConfigContext from '@/context/debug-configuration' +import { AppModeEnum } from '@/types/app' +import Prompt from '../simple-prompt-input' + +const mockEmit = vi.fn() +const mockSetFeatures = vi.fn() +const mockSetShowExternalDataToolModal = vi.fn() +const mockSetModelConfig = vi.fn() +const mockSetPrevPromptConfig = vi.fn() +const mockSetIntroduction = vi.fn() +const mockOnChange = vi.fn() +const mockToastError = vi.fn() + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('@/hooks/use-breakpoints', () => ({ + __esModule: true, + default: () => 'desktop', + MediaType: { + mobile: 'mobile', + }, +})) + +vi.mock('@/app/components/base/features/hooks', () => ({ + useFeaturesStore: () => ({ + getState: () => ({ + features: { + opening: { + enabled: false, + opening_statement: '', + }, + }, + setFeatures: mockSetFeatures, + }), + }), +})) + +vi.mock('@/context/event-emitter', () => ({ + useEventEmitterContextContext: () => ({ + eventEmitter: { + emit: (...args: unknown[]) => mockEmit(...args), + }, + }), +})) + +vi.mock('@/context/modal-context', () => ({ + useModalContext: () => ({ + setShowExternalDataToolModal: mockSetShowExternalDataToolModal, + }), +})) + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + error: (...args: unknown[]) => mockToastError(...args), + }, +})) + +vi.mock('@/app/components/app/configuration/config/automatic/automatic-btn', () => ({ + default: ({ onClick }: { onClick: () => void }) => , +})) + +vi.mock('@/app/components/app/configuration/config/automatic/get-automatic-res', () => ({ + default: ({ onFinished }: { onFinished: (value: Record) => void }) => ( + + ), +})) + +vi.mock('@/app/components/base/prompt-editor', () => ({ + default: (props: { + onBlur: () => void + onChange: (value: string) => void + contextBlock: { datasets: Array<{ id: string, name: string, type: string }> } + variableBlock: { variables: Array<{ name: string, value: string }> } + queryBlock: { selectable: boolean } + externalToolBlock: { + onAddExternalTool: () => void + externalTools: Array<{ name: string, variableName: string }> + } + }) => ( +
+
{`datasets:${props.contextBlock.datasets.map(item => item.name).join(',')}`}
+
{`variables:${props.variableBlock.variables.map(item => item.value).join(',')}`}
+
{`external-tools:${props.externalToolBlock.externalTools.map(item => item.variableName).join(',')}`}
+
{`query-selectable:${String(props.queryBlock.selectable)}`}
+ + + +
+ ), +})) + +vi.mock('../prompt-editor-height-resize-wrap', () => ({ + default: ({ children, footer }: { children: ReactNode, footer: ReactNode }) => ( +
+ {children} + {footer} +
+ ), +})) + +const createContextValue = (overrides: Record = {}) => ({ + appId: 'app-1', + modelConfig: { + configs: { + prompt_template: 'Hello {{new_var}}', + prompt_variables: [ + { key: 'existing_var', name: 'Existing', type: 'string', required: true }, + ], + }, + }, + dataSets: [], + setModelConfig: mockSetModelConfig, + setPrevPromptConfig: mockSetPrevPromptConfig, + setIntroduction: mockSetIntroduction, + hasSetBlockStatus: { + context: false, + history: false, + query: false, + }, + showSelectDataSet: vi.fn(), + externalDataToolsConfig: [], + ...overrides, +}) as any + +describe('SimplePromptInput', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should prompt to add new variables discovered from the prompt template', () => { + render( + + + , + ) + + fireEvent.click(screen.getByText('blur-prompt')) + + expect(screen.getByText('autoAddVar')).toBeInTheDocument() + + fireEvent.click(screen.getByText('operation.add')) + + expect(mockOnChange).toHaveBeenCalledWith('Hello {{new_var}}', [ + expect.objectContaining({ + key: 'new_var', + name: 'new_var', + }), + ]) + }) + + it('should open the external data tool modal and emit insert events after save', () => { + render( + + + , + ) + + fireEvent.click(screen.getByText('open-tool-modal')) + + expect(mockSetShowExternalDataToolModal).toHaveBeenCalledTimes(1) + const modalConfig = mockSetShowExternalDataToolModal.mock.calls[0][0] + + expect(modalConfig.onValidateBeforeSaveCallback({ variable: 'existing_var' })).toBe(false) + expect(mockToastError).toHaveBeenCalledWith('varKeyError.keyAlreadyExists') + expect(modalConfig.onValidateBeforeSaveCallback({ variable: 'fresh_var' })).toBe(true) + + modalConfig.onSaveCallback(undefined) + expect(mockEmit).not.toHaveBeenCalled() + + modalConfig.onSaveCallback({ + label: 'Search', + variable: 'search_api', + }) + + expect(mockEmit).toHaveBeenCalledWith(expect.objectContaining({ + type: 'ADD_EXTERNAL_DATA_TOOL', + })) + expect(mockEmit).toHaveBeenCalledWith(expect.objectContaining({ + payload: 'search_api', + type: INSERT_VARIABLE_VALUE_BLOCK_COMMAND, + })) + }) + + it('should apply automatic generation results to prompt and opening statement', () => { + render( + + + , + ) + + fireEvent.click(screen.getByText('automatic-btn')) + fireEvent.click(screen.getByText('finish-automatic')) + + expect(mockEmit).toHaveBeenCalledWith(expect.objectContaining({ + payload: 'auto prompt', + type: 'PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER', + })) + expect(mockSetModelConfig).toHaveBeenCalledWith(expect.objectContaining({ + configs: expect.objectContaining({ + prompt_template: 'auto prompt', + prompt_variables: [ + expect.objectContaining({ key: 'city', name: 'city' }), + ], + }), + })) + expect(mockSetPrevPromptConfig).toHaveBeenCalled() + expect(mockSetIntroduction).toHaveBeenCalledWith('hello there') + expect(mockSetFeatures).toHaveBeenCalled() + }) + + it('should expose dataset and external tool metadata to the editor', () => { + render( + + + , + ) + + expect(screen.getByText('datasets:Knowledge Base')).toBeInTheDocument() + expect(screen.getByText('variables:existing_var')).toBeInTheDocument() + expect(screen.getByText('external-tools:search_api')).toBeInTheDocument() + expect(screen.getByText('query-selectable:false')).toBeInTheDocument() + }) + + it('should skip external tool variables and incomplete prompt variables when deciding whether to auto add', () => { + render( + + + , + ) + + fireEvent.click(screen.getByText('change-prompt')) + expect(mockOnChange).toHaveBeenCalledWith('Hello {{new_var}}', []) + + fireEvent.click(screen.getByText('blur-prompt')) + expect(mockOnChange).toHaveBeenLastCalledWith('Hello {{search_api}} {{existing_var}}', []) + }) + + it('should keep invalid prompt variables in the confirmation flow', () => { + render( + + + , + ) + + fireEvent.click(screen.getByText('blur-prompt')) + expect(screen.getByText('autoAddVar')).toBeInTheDocument() + + fireEvent.click(screen.getByText('operation.cancel')) + expect(mockOnChange).toHaveBeenCalledWith('Hello {{existing_var}}', []) + }) +}) diff --git a/web/app/components/app/configuration/config-prompt/confirm-add-var/index.spec.tsx b/web/app/components/app/configuration/config-prompt/confirm-add-var/__tests__/index.spec.tsx similarity index 93% rename from web/app/components/app/configuration/config-prompt/confirm-add-var/index.spec.tsx rename to web/app/components/app/configuration/config-prompt/confirm-add-var/__tests__/index.spec.tsx index c5a1500c59..1d6aa91552 100644 --- a/web/app/components/app/configuration/config-prompt/confirm-add-var/index.spec.tsx +++ b/web/app/components/app/configuration/config-prompt/confirm-add-var/__tests__/index.spec.tsx @@ -1,8 +1,8 @@ import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' -import ConfirmAddVar from './index' +import ConfirmAddVar from '../index' -vi.mock('../../base/var-highlight', () => ({ +vi.mock('../../../base/var-highlight', () => ({ default: ({ name }: { name: string }) => {name}, })) diff --git a/web/app/components/app/configuration/config-prompt/confirm-add-var/index.tsx b/web/app/components/app/configuration/config-prompt/confirm-add-var/index.tsx index c2f0cb000a..47d9aaa4d2 100644 --- a/web/app/components/app/configuration/config-prompt/confirm-add-var/index.tsx +++ b/web/app/components/app/configuration/config-prompt/confirm-add-var/index.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import VarHighlight from '../../base/var-highlight' -export type IConfirmAddVarProps = { +type IConfirmAddVarProps = { varNameArr: string[] onConfirm: () => void onCancel: () => void diff --git a/web/app/components/app/configuration/config-prompt/conversation-history/edit-modal.spec.tsx b/web/app/components/app/configuration/config-prompt/conversation-history/__tests__/edit-modal.spec.tsx similarity index 97% rename from web/app/components/app/configuration/config-prompt/conversation-history/edit-modal.spec.tsx rename to web/app/components/app/configuration/config-prompt/conversation-history/__tests__/edit-modal.spec.tsx index 2f417fdded..236f9403c9 100644 --- a/web/app/components/app/configuration/config-prompt/conversation-history/edit-modal.spec.tsx +++ b/web/app/components/app/configuration/config-prompt/conversation-history/__tests__/edit-modal.spec.tsx @@ -1,7 +1,7 @@ import type { ConversationHistoriesRole } from '@/models/debug' import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' -import EditModal from './edit-modal' +import EditModal from '../edit-modal' vi.mock('@/app/components/base/modal', () => ({ default: ({ children }: { children: React.ReactNode }) =>
{children}
, diff --git a/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.spec.tsx b/web/app/components/app/configuration/config-prompt/conversation-history/__tests__/history-panel.spec.tsx similarity index 95% rename from web/app/components/app/configuration/config-prompt/conversation-history/history-panel.spec.tsx rename to web/app/components/app/configuration/config-prompt/conversation-history/__tests__/history-panel.spec.tsx index 827986f521..9a34dc2da1 100644 --- a/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.spec.tsx +++ b/web/app/components/app/configuration/config-prompt/conversation-history/__tests__/history-panel.spec.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react' -import HistoryPanel from './history-panel' +import HistoryPanel from '../history-panel' vi.mock('@/app/components/app/configuration/base/operation-btn', () => ({ default: ({ onClick }: { onClick: () => void }) => ( diff --git a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx index e5f1556cc5..da1949af60 100644 --- a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx +++ b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx @@ -33,7 +33,7 @@ import { getNewVar, getVars } from '@/utils/var' import ConfirmAddVar from './confirm-add-var' import PromptEditorHeightResizeWrap from './prompt-editor-height-resize-wrap' -export type ISimplePromptInput = { +type ISimplePromptInput = { mode: AppModeEnum promptTemplate: string promptVariables: PromptVariable[] diff --git a/web/app/components/app/configuration/config-prompt/style.module.css b/web/app/components/app/configuration/config-prompt/style.module.css index 3086441327..224d59d9c8 100644 --- a/web/app/components/app/configuration/config-prompt/style.module.css +++ b/web/app/components/app/configuration/config-prompt/style.module.css @@ -1,5 +1,3 @@ -@reference "../../../../styles/globals.css"; - .gradientBorder { background: radial-gradient(circle at 100% 100%, #fcfcfd 0, #fcfcfd 10px, transparent 10px) 0% 0%/12px 12px no-repeat, radial-gradient(circle at 0 100%, #fcfcfd 0, #fcfcfd 10px, transparent 10px) 100% 0%/12px 12px no-repeat, diff --git a/web/app/components/app/configuration/config-var/index.spec.tsx b/web/app/components/app/configuration/config-var/__tests__/index.spec.tsx similarity index 82% rename from web/app/components/app/configuration/config-var/index.spec.tsx rename to web/app/components/app/configuration/config-var/__tests__/index.spec.tsx index a48d3233f5..fb190d844a 100644 --- a/web/app/components/app/configuration/config-var/index.spec.tsx +++ b/web/app/components/app/configuration/config-var/__tests__/index.spec.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react' -import type { IConfigVarProps } from './index' +import type { IConfigVarProps } from '../index' import type { ExternalDataTool } from '@/models/common' import type { PromptVariable } from '@/models/debug' import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react' @@ -9,7 +9,7 @@ import { toast } from '@/app/components/base/ui/toast' import DebugConfigurationContext from '@/context/debug-configuration' import { AppModeEnum } from '@/types/app' -import ConfigVar, { ADD_EXTERNAL_DATA_TOOL } from './index' +import ConfigVar, { ADD_EXTERNAL_DATA_TOOL } from '../index' const toastErrorSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error') @@ -393,5 +393,94 @@ describe('ConfigVar', () => { }), ]) }) + + it('should update an api variable with the modal save callback', () => { + const onPromptVariablesChange = vi.fn() + const apiVar = createPromptVariable({ + key: 'api_var', + name: 'API Var', + type: 'api', + }) + + renderConfigVar({ + promptVariables: [apiVar], + onPromptVariablesChange, + }) + + const item = screen.getByTitle('api_var · API Var') + const itemContainer = item.closest('div.group') + expect(itemContainer).not.toBeNull() + + const actionButtons = itemContainer!.querySelectorAll('div.h-6.w-6') + fireEvent.click(actionButtons[0]) + + const modalState = setShowExternalDataToolModal.mock.calls.at(-1)?.[0] + + act(() => { + modalState.onSaveCallback?.({ + variable: 'next_api_var', + label: 'Next API Var', + enabled: true, + type: 'api', + config: { endpoint: '/search' }, + icon: 'tool-icon', + icon_background: '#fff', + }) + }) + + expect(onPromptVariablesChange).toHaveBeenCalledWith([ + expect.objectContaining({ + key: 'next_api_var', + name: 'Next API Var', + type: 'api', + icon: 'tool-icon', + }), + ]) + }) + + it('should ignore empty external tool saves and reject duplicate variable names during validation', () => { + const onPromptVariablesChange = vi.fn() + const firstVar = createPromptVariable({ + key: 'api_var', + name: 'API Var', + type: 'api', + }) + const secondVar = createPromptVariable({ + key: 'existing_var', + name: 'Existing Var', + type: 'string', + }) + + renderConfigVar({ + promptVariables: [firstVar, secondVar], + onPromptVariablesChange, + }) + + const item = screen.getByTitle('api_var · API Var') + const itemContainer = item.closest('div.group') + expect(itemContainer).not.toBeNull() + + const actionButtons = itemContainer!.querySelectorAll('div.h-6.w-6') + fireEvent.click(actionButtons[0]) + + const modalState = setShowExternalDataToolModal.mock.calls.at(-1)?.[0] + + act(() => { + modalState.onSaveCallback?.(undefined) + }) + + expect(onPromptVariablesChange).not.toHaveBeenCalled() + + const isValid = modalState.onValidateBeforeSaveCallback?.({ + variable: 'existing_var', + label: 'Duplicated', + enabled: true, + type: 'api', + config: {}, + }) + + expect(isValid).toBe(false) + expect(toastErrorSpy).toHaveBeenCalled() + }) }) }) diff --git a/web/app/components/app/configuration/config-var/config-modal/__tests__/form-fields.spec.tsx b/web/app/components/app/configuration/config-var/config-modal/__tests__/form-fields.spec.tsx new file mode 100644 index 0000000000..0740f0cde3 --- /dev/null +++ b/web/app/components/app/configuration/config-var/config-modal/__tests__/form-fields.spec.tsx @@ -0,0 +1,207 @@ +/* eslint-disable ts/no-explicit-any */ +import type { ReactNode } from 'react' +import { fireEvent, render, screen } from '@testing-library/react' +import { InputVarType } from '@/app/components/workflow/types' +import ConfigModalFormFields from '../form-fields' + +vi.mock('@/app/components/base/file-uploader', () => ({ + FileUploaderInAttachmentWrapper: ({ onChange }: { onChange: (files: Array>) => void }) => ( + + ), +})) + +vi.mock('@/app/components/workflow/nodes/_base/components/file-upload-setting', () => ({ + default: ({ onChange, isMultiple }: { onChange: (payload: Record) => void, isMultiple: boolean }) => ( + + ), +})) + +vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({ + default: ({ onChange }: { onChange: (value: string) => void }) => ( + + ), +})) + +vi.mock('@/app/components/base/checkbox', () => ({ + default: ({ onCheck, checked }: { onCheck: () => void, checked: boolean }) => ( + + ), +})) + +vi.mock('@/app/components/base/select', () => ({ + default: ({ onSelect }: { onSelect: (item: { value: string }) => void }) => ( + + ), +})) + +vi.mock('@/app/components/base/ui/select', () => ({ + Select: ({ value, onValueChange, children }: { value: string, onValueChange: (value: string) => void, children: ReactNode }) => ( +
+ + {children} +
+ ), + SelectTrigger: ({ children }: { children: ReactNode }) =>
{children}
, + SelectValue: () => select-value, + SelectContent: ({ children }: { children: ReactNode }) =>
{children}
, + SelectItem: ({ children }: { children: ReactNode }) =>
{children}
, +})) + +vi.mock('../field', () => ({ + default: ({ children, title }: { children: ReactNode, title: string }) => ( +
+ {title} + {children} +
+ ), +})) + +vi.mock('../type-select', () => ({ + default: ({ onSelect }: { onSelect: (item: { value: InputVarType }) => void }) => ( + + ), +})) + +vi.mock('../../config-select', () => ({ + default: ({ onChange }: { onChange: (value: string[]) => void }) => ( + + ), +})) + +vi.mock('../../config-string', () => ({ + default: ({ onChange }: { onChange: (value: number) => void }) => ( + + ), +})) + +const t = (key: string) => key + +const createPayloadChangeHandler = () => vi.fn<(value: unknown) => void>() + +const createBaseProps = () => { + const payloadChangeHandlers: Record> = { + default: createPayloadChangeHandler(), + hide: createPayloadChangeHandler(), + label: createPayloadChangeHandler(), + max_length: createPayloadChangeHandler(), + options: createPayloadChangeHandler(), + required: createPayloadChangeHandler(), + } + + return { + checkboxDefaultSelectValue: 'false', + isStringInput: false, + jsonSchemaStr: '', + maxLength: 32, + modelId: 'gpt-4o', + onFilePayloadChange: vi.fn(), + onJSONSchemaChange: vi.fn(), + onPayloadChange: (key: string) => { + if (!payloadChangeHandlers[key]) + payloadChangeHandlers[key] = createPayloadChangeHandler() + return payloadChangeHandlers[key] + }, + onTypeChange: vi.fn(), + onVarKeyBlur: vi.fn(), + onVarNameChange: vi.fn(), + options: undefined as string[] | undefined, + selectOptions: [], + tempPayload: { + type: InputVarType.textInput, + label: 'Question', + variable: 'question', + required: false, + hide: false, + } as any, + t, + payloadChangeHandlers, + } +} + +describe('ConfigModalFormFields', () => { + it('should update paragraph, number, checkbox, and select defaults', () => { + const paragraphProps = createBaseProps() + paragraphProps.tempPayload = { ...paragraphProps.tempPayload, type: InputVarType.paragraph, default: 'hello' } + render() + fireEvent.change(screen.getByDisplayValue('hello'), { target: { value: 'updated paragraph' } }) + expect(paragraphProps.payloadChangeHandlers.default).toHaveBeenCalledWith('updated paragraph') + + const numberProps = createBaseProps() + numberProps.tempPayload = { ...numberProps.tempPayload, type: InputVarType.number, default: '1' } + render() + fireEvent.change(screen.getByDisplayValue('1'), { target: { value: '2' } }) + expect(numberProps.payloadChangeHandlers.default).toHaveBeenCalledWith('2') + + const checkboxProps = createBaseProps() + checkboxProps.tempPayload = { ...checkboxProps.tempPayload, type: InputVarType.checkbox, default: false } + checkboxProps.checkboxDefaultSelectValue = 'true' + render() + fireEvent.click(screen.getByText('ui-select:true')) + expect(checkboxProps.payloadChangeHandlers.default).toHaveBeenCalledWith(false) + + const selectProps = createBaseProps() + selectProps.tempPayload = { ...selectProps.tempPayload, type: InputVarType.select, default: 'alpha' } + selectProps.options = ['alpha', 'beta'] + render() + fireEvent.click(screen.getByText('config-select')) + fireEvent.click(screen.getByText('ui-select:alpha')) + expect(selectProps.payloadChangeHandlers.options).toHaveBeenCalledWith(['alpha', 'beta']) + expect(selectProps.payloadChangeHandlers.default).toHaveBeenCalledWith('beta') + }) + + it('should wire file, json schema, and visibility controls', () => { + const singleFileProps = createBaseProps() + singleFileProps.tempPayload = { + ...singleFileProps.tempPayload, + type: InputVarType.singleFile, + allowed_file_types: ['document'], + allowed_file_extensions: [], + allowed_file_upload_methods: ['remote_url'], + } + render() + fireEvent.click(screen.getByText('single-file-setting')) + fireEvent.click(screen.getByText('upload-file')) + fireEvent.click(screen.getAllByText('unchecked')[0]) + fireEvent.click(screen.getAllByText('unchecked')[1]) + + expect(singleFileProps.onFilePayloadChange).toHaveBeenCalledWith({ number_limits: 1 }) + expect(singleFileProps.payloadChangeHandlers.default).toHaveBeenCalledWith(expect.objectContaining({ + fileId: 'file-1', + })) + expect(singleFileProps.payloadChangeHandlers.required).toHaveBeenCalledWith(true) + expect(singleFileProps.payloadChangeHandlers.hide).toHaveBeenCalledWith(true) + + const multiFileProps = createBaseProps() + multiFileProps.tempPayload = { + ...multiFileProps.tempPayload, + type: InputVarType.multiFiles, + allowed_file_types: ['document'], + allowed_file_extensions: [], + allowed_file_upload_methods: ['remote_url'], + } + render() + fireEvent.click(screen.getByText('multi-file-setting')) + fireEvent.click(screen.getAllByText('upload-file')[1]) + expect(multiFileProps.onFilePayloadChange).toHaveBeenCalledWith({ number_limits: 3 }) + expect(multiFileProps.payloadChangeHandlers.default).toHaveBeenCalledWith([ + expect.objectContaining({ fileId: 'file-1' }), + expect.objectContaining({ fileId: 'file-2' }), + ]) + + const jsonProps = createBaseProps() + jsonProps.tempPayload = { ...jsonProps.tempPayload, type: InputVarType.jsonObject } + render() + fireEvent.click(screen.getByText('json-editor')) + expect(jsonProps.onJSONSchemaChange).toHaveBeenCalledWith('{\n "type": "object"\n}') + }) +}) diff --git a/web/app/components/app/configuration/config-var/config-modal/__tests__/index-logic.spec.tsx b/web/app/components/app/configuration/config-var/config-modal/__tests__/index-logic.spec.tsx new file mode 100644 index 0000000000..4888d284d2 --- /dev/null +++ b/web/app/components/app/configuration/config-var/config-modal/__tests__/index-logic.spec.tsx @@ -0,0 +1,150 @@ +/* eslint-disable ts/no-explicit-any */ +import type { InputVar } from '@/app/components/workflow/types' +import type { App, AppSSO } from '@/types/app' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import * as React from 'react' +import { useStore } from '@/app/components/app/store' +import { toast } from '@/app/components/base/ui/toast' +import { InputVarType } from '@/app/components/workflow/types' +import DebugConfigurationContext from '@/context/debug-configuration' +import { AppModeEnum } from '@/types/app' +import ConfigModal from '../index' + +const toastErrorSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error') +let latestFormProps: Record | null = null + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('../form-fields', () => ({ + default: (props: Record) => { + latestFormProps = props + return ( +
+
{String(props.tempPayload.type)}
+
{String(props.tempPayload.label ?? '')}
+
{String(props.tempPayload.json_schema ?? '')}
+
{String(props.tempPayload.default ?? '')}
+ + + + + + + + +
+ ) + }, +})) + +const createPayload = (overrides: Partial = {}): InputVar => ({ + type: InputVarType.textInput, + label: '', + variable: 'question', + required: false, + hide: false, + options: [], + default: 'hello', + max_length: 32, + ...overrides, +}) + +const renderConfigModal = (payload: InputVar = createPayload()) => render( + + + , +) + +describe('ConfigModal logic', () => { + beforeEach(() => { + vi.clearAllMocks() + latestFormProps = null + useStore.setState({ + appDetail: { + mode: AppModeEnum.CHAT, + } as App & Partial, + }) + }) + + it('should surface validation errors from invalid variable name callbacks', async () => { + renderConfigModal() + + fireEvent.click(screen.getByTestId('invalid-key-blur')) + fireEvent.click(screen.getByTestId('invalid-name-change')) + + await waitFor(() => { + expect(toastErrorSpy).toHaveBeenCalledTimes(2) + }) + }) + + it('should keep the existing label when blur runs on a payload that already has one', async () => { + renderConfigModal(createPayload({ label: 'Existing label' })) + + fireEvent.click(screen.getByTestId('valid-key-blur')) + + await waitFor(() => { + expect(screen.getByTestId('payload-label')).toHaveTextContent('Existing label') + }) + }) + + it('should derive payload fields from mocked form-field callbacks', async () => { + renderConfigModal() + + fireEvent.click(screen.getByTestId('valid-key-blur')) + await waitFor(() => { + expect(screen.getByTestId('payload-label')).toHaveTextContent('auto_label') + }) + + fireEvent.click(screen.getByTestId('valid-json-change')) + await waitFor(() => { + expect(screen.getByTestId('payload-schema')).toHaveTextContent(/"foo": "bar"/) + }) + + fireEvent.click(screen.getByTestId('invalid-json-change')) + expect(screen.getByTestId('payload-schema')).toHaveTextContent(/"foo": "bar"/) + + fireEvent.click(screen.getByTestId('empty-json-change')) + await waitFor(() => { + expect(screen.getByTestId('payload-schema')).toHaveTextContent('') + }) + + fireEvent.click(screen.getByTestId('type-change')) + await waitFor(() => { + expect(screen.getByTestId('payload-type')).toHaveTextContent(InputVarType.singleFile) + }) + + fireEvent.click(screen.getByTestId('file-payload-change')) + await waitFor(() => { + expect(screen.getByTestId('payload-default')).toHaveTextContent('file-default') + }) + + expect(latestFormProps?.modelId).toBe('model-1') + }) +}) diff --git a/web/app/components/app/configuration/config-var/config-modal/__tests__/index.spec.tsx b/web/app/components/app/configuration/config-var/config-modal/__tests__/index.spec.tsx new file mode 100644 index 0000000000..31256f0c08 --- /dev/null +++ b/web/app/components/app/configuration/config-var/config-modal/__tests__/index.spec.tsx @@ -0,0 +1,89 @@ +import type { InputVar } from '@/app/components/workflow/types' +import type { App, AppSSO } from '@/types/app' +import { fireEvent, render, screen } from '@testing-library/react' +import * as React from 'react' +import { useStore } from '@/app/components/app/store' +import { toast } from '@/app/components/base/ui/toast' +import { InputVarType } from '@/app/components/workflow/types' +import { AppModeEnum } from '@/types/app' +import ConfigModal from '../index' + +const toastErrorSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error') + +const createPayload = (overrides: Partial = {}): InputVar => ({ + type: InputVarType.textInput, + label: '', + variable: 'question', + required: false, + hide: false, + options: [], + default: 'hello', + max_length: 32, + ...overrides, +}) + +describe('ConfigModal', () => { + beforeEach(() => { + vi.clearAllMocks() + useStore.setState({ + appDetail: { + mode: AppModeEnum.CHAT, + } as App & Partial, + }) + }) + + it('should copy the variable name into the label when the label is empty', () => { + render( + , + ) + + const textboxes = screen.getAllByRole('textbox') + fireEvent.blur(textboxes[0], { target: { value: 'question' } }) + + expect(textboxes[1]).toHaveValue('question') + }) + + it('should submit the edited payload when the form is valid', () => { + const onConfirm = vi.fn() + render( + , + ) + + fireEvent.change(screen.getByDisplayValue('hello'), { target: { value: 'updated default' } }) + fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' })) + + expect(onConfirm).toHaveBeenCalledWith(expect.objectContaining({ + default: 'updated default', + label: 'Question', + variable: 'question', + }), undefined) + }) + + it('should block save when the label is missing', () => { + render( + , + ) + + fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' })) + + expect(toastErrorSpy).toHaveBeenCalledWith('appDebug.variableConfig.errorMsg.labelNameRequired') + }) +}) diff --git a/web/app/components/app/configuration/config-var/config-modal/__tests__/type-select.spec.tsx b/web/app/components/app/configuration/config-var/config-modal/__tests__/type-select.spec.tsx new file mode 100644 index 0000000000..2512aa93e8 --- /dev/null +++ b/web/app/components/app/configuration/config-var/config-modal/__tests__/type-select.spec.tsx @@ -0,0 +1,37 @@ +/* eslint-disable ts/no-explicit-any */ +import { fireEvent, render, screen } from '@testing-library/react' +import TypeSelector from '../type-select' + +vi.mock('@/app/components/base/portal-to-follow-elem', () => ({ + PortalToFollowElem: ({ children }: { children: React.ReactNode }) =>
{children}
, + PortalToFollowElemTrigger: ({ children, onClick }: { children: React.ReactNode, onClick?: () => void }) => ( + + ), + PortalToFollowElemContent: ({ children }: { children: React.ReactNode }) =>
{children}
, +})) + +vi.mock('@/app/components/workflow/nodes/_base/components/input-var-type-icon', () => ({ + default: ({ type }: { type: string }) => {type}, +})) + +describe('TypeSelector', () => { + it('should toggle open state and select a new variable type', () => { + const onSelect = vi.fn() + + render( + , + ) + + fireEvent.click(screen.getByRole('button')) + fireEvent.click(screen.getByText('Number')) + + expect(onSelect).toHaveBeenCalledWith({ value: 'number', name: 'Number' }) + }) +}) diff --git a/web/app/components/app/configuration/config-var/config-modal/__tests__/utils.spec.ts b/web/app/components/app/configuration/config-var/config-modal/__tests__/utils.spec.ts new file mode 100644 index 0000000000..1c00e1c5b2 --- /dev/null +++ b/web/app/components/app/configuration/config-var/config-modal/__tests__/utils.spec.ts @@ -0,0 +1,267 @@ +import type { InputVar } from '@/app/components/workflow/types' +import { DEFAULT_FILE_UPLOAD_SETTING } from '@/app/components/workflow/constants' +import { ChangeType, InputVarType, SupportUploadFileTypes } from '@/app/components/workflow/types' +import { + buildSelectOptions, + createPayloadForType, + getCheckboxDefaultSelectValue, + getJsonSchemaEditorValue, + isJsonSchemaEmpty, + isStringInputType, + normalizeSelectDefaultValue, + parseCheckboxSelectValue, + updatePayloadField, + validateConfigModalPayload, +} from '../utils' + +const t = (key: string) => key + +const createInputVar = (overrides: Partial = {}): InputVar => ({ + type: InputVarType.textInput, + label: 'Question', + variable: 'question', + required: false, + options: [], + hide: false, + ...overrides, +}) + +describe('config-modal utils', () => { + describe('payload helpers', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should clear the default value when options no longer include it', () => { + const payload = createInputVar({ + type: InputVarType.select, + default: 'beta', + options: ['alpha', 'beta'], + }) + + const nextPayload = updatePayloadField(payload, 'options', ['alpha']) + + expect(nextPayload.default).toBeUndefined() + expect(nextPayload.options).toEqual(['alpha']) + }) + + it('should seed upload defaults when switching to multi-file input', () => { + const payload = createInputVar({ + type: InputVarType.textInput, + default: 'hello', + }) + + const nextPayload = createPayloadForType(payload, InputVarType.multiFiles) + + expect(nextPayload.type).toBe(InputVarType.multiFiles) + expect(nextPayload.max_length).toBe(DEFAULT_FILE_UPLOAD_SETTING.max_length) + expect(nextPayload.allowed_file_types).toEqual(DEFAULT_FILE_UPLOAD_SETTING.allowed_file_types) + expect(nextPayload.default).toBe('hello') + }) + + it('should clear the default value when switching to a select input type', () => { + const payload = createInputVar({ + type: InputVarType.textInput, + default: 'hello', + }) + + const nextPayload = createPayloadForType(payload, InputVarType.select) + + expect(nextPayload.type).toBe(InputVarType.select) + expect(nextPayload.default).toBeUndefined() + }) + + it('should normalize empty select defaults to undefined', () => { + const nextPayload = normalizeSelectDefaultValue(createInputVar({ + type: InputVarType.select, + default: '', + })) + + expect(nextPayload.default).toBeUndefined() + }) + + it('should parse checkbox default values and normalize json schema editor content', () => { + expect(parseCheckboxSelectValue('true')).toBe(true) + expect(parseCheckboxSelectValue('false')).toBe(false) + expect(getJsonSchemaEditorValue(InputVarType.jsonObject, { type: 'object' } as never)).toBe(JSON.stringify({ type: 'object' }, null, 2)) + expect(getJsonSchemaEditorValue(InputVarType.textInput, '{"type":"object"}')).toBe('') + expect(getJsonSchemaEditorValue(InputVarType.jsonObject, '{"type":"object"}')).toBe('{"type":"object"}') + }) + + it('should fall back to an empty editor value when json schema serialization fails', () => { + const circular: Record = {} + circular.self = circular + + expect(getJsonSchemaEditorValue(InputVarType.jsonObject, circular as never)).toBe('') + }) + }) + + describe('derived values', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should expose upload and json options only when supported', () => { + const options = buildSelectOptions({ + isBasicApp: false, + supportFile: true, + t, + }) + + expect(options.map(option => option.value)).toEqual(expect.arrayContaining([ + InputVarType.singleFile, + InputVarType.multiFiles, + InputVarType.jsonObject, + ])) + }) + + it('should derive checkbox defaults from boolean and string values', () => { + expect(getCheckboxDefaultSelectValue(true)).toBe('true') + expect(getCheckboxDefaultSelectValue('TRUE')).toBe('true') + expect(getCheckboxDefaultSelectValue(undefined)).toBe('false') + }) + + it('should detect blank json schema values', () => { + expect(isJsonSchemaEmpty(undefined)).toBe(true) + expect(isJsonSchemaEmpty(' ')).toBe(true) + expect(isJsonSchemaEmpty('{}')).toBe(false) + expect(isJsonSchemaEmpty({ type: 'object' } as never)).toBe(false) + expect(isStringInputType(InputVarType.textInput)).toBe(true) + expect(isStringInputType(InputVarType.paragraph)).toBe(true) + expect(isStringInputType(InputVarType.number)).toBe(false) + }) + }) + + describe('validation', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should reject duplicate select options', () => { + const checkVariableName = vi.fn(() => true) + + const result = validateConfigModalPayload({ + tempPayload: createInputVar({ + type: InputVarType.select, + options: ['alpha', 'alpha'], + }), + checkVariableName, + payload: createInputVar({ + variable: 'question', + }), + t, + }) + + expect(result.errorMessage).toBe('variableConfig.errorMsg.optionRepeat') + expect(checkVariableName).toHaveBeenCalledWith('question') + }) + + it('should require custom extensions when custom file types are enabled', () => { + const result = validateConfigModalPayload({ + tempPayload: createInputVar({ + type: InputVarType.singleFile, + allowed_file_types: [SupportUploadFileTypes.custom], + allowed_file_extensions: [], + }), + checkVariableName: () => true, + payload: createInputVar(), + t, + }) + + expect(result.errorMessage).toBe('errorMsg.fieldRequired') + }) + + it('should require at least one select option and supported file types', () => { + const selectResult = validateConfigModalPayload({ + tempPayload: createInputVar({ + type: InputVarType.select, + options: [], + }), + checkVariableName: () => true, + payload: createInputVar(), + t, + }) + + const fileResult = validateConfigModalPayload({ + tempPayload: createInputVar({ + type: InputVarType.singleFile, + allowed_file_types: [], + }), + checkVariableName: () => true, + payload: createInputVar(), + t, + }) + + expect(selectResult.errorMessage).toBe('variableConfig.errorMsg.atLeastOneOption') + expect(fileResult.errorMessage).toBe('errorMsg.fieldRequired') + }) + + it('should reject invalid json schema definitions', () => { + const invalidResult = validateConfigModalPayload({ + tempPayload: createInputVar({ + type: InputVarType.jsonObject, + json_schema: '{', + }), + payload: createInputVar(), + checkVariableName: () => true, + t, + }) + + const nonObjectResult = validateConfigModalPayload({ + tempPayload: createInputVar({ + type: InputVarType.jsonObject, + json_schema: JSON.stringify({ type: 'string' }), + }), + payload: createInputVar(), + checkVariableName: () => true, + t, + }) + + expect(invalidResult.errorMessage).toBe('variableConfig.errorMsg.jsonSchemaInvalid') + expect(nonObjectResult.errorMessage).toBe('variableConfig.errorMsg.jsonSchemaMustBeObject') + }) + + it('should normalize blank json schema and return rename metadata', () => { + const result = validateConfigModalPayload({ + tempPayload: createInputVar({ + type: InputVarType.jsonObject, + variable: 'question_new', + json_schema: ' ', + }), + payload: createInputVar({ + variable: 'question_old', + }), + checkVariableName: () => true, + t, + }) + + expect(result.errorMessage).toBeUndefined() + expect(result.payloadToSave).toEqual(expect.objectContaining({ + json_schema: undefined, + variable: 'question_new', + })) + expect(result.moreInfo).toEqual({ + type: ChangeType.changeVarName, + payload: { + beforeKey: 'question_old', + afterKey: 'question_new', + }, + }) + }) + + it('should stop validation when the variable name checker rejects the payload', () => { + const result = validateConfigModalPayload({ + tempPayload: createInputVar({ + variable: 'invalid_name', + }), + payload: createInputVar({ + variable: 'question', + }), + checkVariableName: () => false, + t, + }) + + expect(result).toEqual({}) + }) + }) +}) diff --git a/web/app/components/app/configuration/config-var/config-modal/config.ts b/web/app/components/app/configuration/config-var/config-modal/config.ts index 6586c2fd54..e03464d453 100644 --- a/web/app/components/app/configuration/config-var/config-modal/config.ts +++ b/web/app/components/app/configuration/config-var/config-modal/config.ts @@ -1,10 +1,3 @@ -export const jsonObjectWrap = { - type: 'object', - properties: {}, - required: [], - additionalProperties: true, -} - export const jsonConfigPlaceHolder = JSON.stringify( { type: 'object', diff --git a/web/app/components/app/configuration/config-var/config-modal/form-fields.tsx b/web/app/components/app/configuration/config-var/config-modal/form-fields.tsx new file mode 100644 index 0000000000..c2a02f4710 --- /dev/null +++ b/web/app/components/app/configuration/config-var/config-modal/form-fields.tsx @@ -0,0 +1,228 @@ +'use client' +import type { ChangeEvent, FC } from 'react' +import type { Item as SelectOptionItem } from './type-select' +import type { FileEntity } from '@/app/components/base/file-uploader/types' +import type { InputVar, UploadFileSetting } from '@/app/components/workflow/types' +import * as React from 'react' +import Checkbox from '@/app/components/base/checkbox' +import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader' +import Input from '@/app/components/base/input' +import Textarea from '@/app/components/base/textarea' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/app/components/base/ui/select' +import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' +import FileUploadSetting from '@/app/components/workflow/nodes/_base/components/file-upload-setting' +import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' +import { InputVarType, SupportUploadFileTypes } from '@/app/components/workflow/types' +import { TransferMethod } from '@/types/app' +import ConfigSelect from '../config-select' +import ConfigString from '../config-string' +import { jsonConfigPlaceHolder } from './config' +import Field from './field' +import TypeSelector from './type-select' +import { CHECKBOX_DEFAULT_FALSE_VALUE, CHECKBOX_DEFAULT_TRUE_VALUE, TEXT_MAX_LENGTH } from './utils' + +type Translate = (key: string, options?: Record) => string +const EMPTY_SELECT_VALUE = '__empty__' + +type ConfigModalFormFieldsProps = { + checkboxDefaultSelectValue: string + isStringInput: boolean + jsonSchemaStr: string + maxLength?: number + modelId: string + onFilePayloadChange: (payload: UploadFileSetting) => void + onJSONSchemaChange: (value: string) => void + onPayloadChange: (key: string) => (value: unknown) => void + onTypeChange: (item: SelectOptionItem) => void + onVarKeyBlur: (event: ChangeEvent) => void + onVarNameChange: (event: ChangeEvent) => void + options?: string[] + selectOptions: SelectOptionItem[] + tempPayload: InputVar + t: Translate +} + +const ConfigModalFormFields: FC = ({ + checkboxDefaultSelectValue, + isStringInput, + jsonSchemaStr, + maxLength, + modelId, + onFilePayloadChange, + onJSONSchemaChange, + onPayloadChange, + onTypeChange, + onVarKeyBlur, + onVarNameChange, + options, + selectOptions, + tempPayload, + t, +}) => { + const { type, label, variable } = tempPayload + + return ( +
+ + + + + + + + + onPayloadChange('label')(e.target.value)} + placeholder={t('variableConfig.inputPlaceholder', { ns: 'appDebug' })} + /> + + + {isStringInput && ( + + + + )} + + {type === InputVarType.textInput && ( + + onPayloadChange('default')(e.target.value || undefined)} + placeholder={t('variableConfig.inputPlaceholder', { ns: 'appDebug' })} + /> + + )} + + {type === InputVarType.paragraph && ( + +