Merge branch 'main' into fix/template-textarea-prefill

This commit is contained in:
crazywoola 2025-12-29 09:48:03 +08:00 committed by GitHub
commit 82e8e419fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 1210 additions and 279 deletions

8
.claude/settings.json Normal file
View File

@ -0,0 +1,8 @@
{
"enabledPlugins": {
"feature-dev@claude-plugins-official": true,
"context7@claude-plugins-official": true,
"typescript-lsp@claude-plugins-official": true,
"pyright-lsp@claude-plugins-official": true
}
}

View File

@ -1,19 +0,0 @@
{
"permissions": {
"allow": [],
"deny": []
},
"env": {
"__comment": "Environment variables for MCP servers. Override in .claude/settings.local.json with actual values.",
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"enabledMcpjsonServers": [
"context7",
"sequential-thinking",
"github",
"fetch",
"playwright",
"ide"
],
"enableAllProjectMcpServers": true
}

View File

@ -1,34 +0,0 @@
{
"mcpServers": {
"context7": {
"type": "http",
"url": "https://mcp.context7.com/mcp"
},
"sequential-thinking": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking"],
"env": {}
},
"github": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
}
},
"fetch": {
"type": "stdio",
"command": "uvx",
"args": ["mcp-server-fetch"],
"env": {}
},
"playwright": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@playwright/mcp@latest"],
"env": {}
}
}
}

View File

@ -313,17 +313,20 @@ class StreamableHTTPTransport:
if is_initialization:
self._maybe_extract_session_id_from_response(response)
content_type = cast(str, response.headers.get(CONTENT_TYPE, "").lower())
# Per https://modelcontextprotocol.io/specification/2025-06-18/basic#notifications:
# The server MUST NOT send a response to notifications.
if isinstance(message.root, JSONRPCRequest):
content_type = cast(str, response.headers.get(CONTENT_TYPE, "").lower())
if content_type.startswith(JSON):
self._handle_json_response(response, ctx.server_to_client_queue)
elif content_type.startswith(SSE):
self._handle_sse_response(response, ctx)
else:
self._handle_unexpected_content_type(
content_type,
ctx.server_to_client_queue,
)
if content_type.startswith(JSON):
self._handle_json_response(response, ctx.server_to_client_queue)
elif content_type.startswith(SSE):
self._handle_sse_response(response, ctx)
else:
self._handle_unexpected_content_type(
content_type,
ctx.server_to_client_queue,
)
def _handle_json_response(
self,

View File

@ -14,7 +14,8 @@ from enums.quota_type import QuotaType, unlimited
from extensions.otel import AppGenerateHandler, trace_span
from models.model import Account, App, AppMode, EndUser
from models.workflow import Workflow
from services.errors.app import InvokeRateLimitError, QuotaExceededError, WorkflowIdFormatError, WorkflowNotFoundError
from services.errors.app import QuotaExceededError, WorkflowIdFormatError, WorkflowNotFoundError
from services.errors.llm import InvokeRateLimitError
from services.workflow_service import WorkflowService

View File

@ -21,7 +21,7 @@ from models.model import App, EndUser
from models.trigger import WorkflowTriggerLog
from models.workflow import Workflow
from repositories.sqlalchemy_workflow_trigger_log_repository import SQLAlchemyWorkflowTriggerLogRepository
from services.errors.app import InvokeRateLimitError, QuotaExceededError, WorkflowNotFoundError
from services.errors.app import QuotaExceededError, WorkflowNotFoundError, WorkflowQuotaLimitError
from services.workflow.entities import AsyncTriggerResponse, TriggerData, WorkflowTaskData
from services.workflow.queue_dispatcher import QueueDispatcherManager, QueuePriority
from services.workflow_service import WorkflowService
@ -141,7 +141,7 @@ class AsyncWorkflowService:
trigger_log_repo.update(trigger_log)
session.commit()
raise InvokeRateLimitError(
raise WorkflowQuotaLimitError(
f"Workflow execution quota limit reached for tenant {trigger_data.tenant_id}"
) from e

View File

@ -18,8 +18,8 @@ class WorkflowIdFormatError(Exception):
pass
class InvokeRateLimitError(Exception):
"""Raised when rate limit is exceeded for workflow invocations."""
class WorkflowQuotaLimitError(Exception):
"""Raised when workflow execution quota is exceeded (for async/background workflows)."""
pass

View File

@ -146,7 +146,7 @@ class PluginParameterService:
provider,
action,
resolved_credentials,
CredentialType.API_KEY.value,
original_subscription.credential_type or CredentialType.UNAUTHORIZED.value,
parameter,
)
.options

View File

@ -868,48 +868,111 @@ class TriggerProviderService:
if not provider_controller:
raise ValueError(f"Provider {provider_id} not found")
subscription = TriggerProviderService.get_subscription_by_id(
tenant_id=tenant_id,
subscription_id=subscription_id,
)
if not subscription:
raise ValueError(f"Subscription {subscription_id} not found")
# Use distributed lock to prevent race conditions on the same subscription
lock_key = f"trigger_subscription_rebuild_lock:{tenant_id}_{subscription_id}"
with redis_client.lock(lock_key, timeout=20):
with Session(db.engine, expire_on_commit=False) as session:
try:
# Get subscription within the transaction
subscription: TriggerSubscription | None = (
session.query(TriggerSubscription).filter_by(tenant_id=tenant_id, id=subscription_id).first()
)
if not subscription:
raise ValueError(f"Subscription {subscription_id} not found")
credential_type = CredentialType.of(subscription.credential_type)
if credential_type not in [CredentialType.OAUTH2, CredentialType.API_KEY]:
raise ValueError("Credential type not supported for rebuild")
credential_type = CredentialType.of(subscription.credential_type)
if credential_type not in [CredentialType.OAUTH2, CredentialType.API_KEY]:
raise ValueError("Credential type not supported for rebuild")
# TODO: Trying to invoke update api of the plugin trigger provider
# Decrypt existing credentials for merging
credential_encrypter, _ = create_trigger_provider_encrypter_for_subscription(
tenant_id=tenant_id,
controller=provider_controller,
subscription=subscription,
)
decrypted_credentials = dict(credential_encrypter.decrypt(subscription.credentials))
# FALLBACK: If the update api is not implemented, delete the previous subscription and create a new one
# Merge credentials: if caller passed HIDDEN_VALUE, retain existing decrypted value
merged_credentials: dict[str, Any] = {
key: value if value != HIDDEN_VALUE else decrypted_credentials.get(key, UNKNOWN_VALUE)
for key, value in credentials.items()
}
# Delete the previous subscription
user_id = subscription.user_id
TriggerManager.unsubscribe_trigger(
tenant_id=tenant_id,
user_id=user_id,
provider_id=provider_id,
subscription=subscription.to_entity(),
credentials=subscription.credentials,
credential_type=credential_type,
)
user_id = subscription.user_id
# Create a new subscription with the same subscription_id and endpoint_id
new_subscription: TriggerSubscriptionEntity = TriggerManager.subscribe_trigger(
tenant_id=tenant_id,
user_id=user_id,
provider_id=provider_id,
endpoint=generate_plugin_trigger_endpoint_url(subscription.endpoint_id),
parameters=parameters,
credentials=credentials,
credential_type=credential_type,
)
TriggerProviderService.update_trigger_subscription(
tenant_id=tenant_id,
subscription_id=subscription.id,
name=name,
parameters=parameters,
credentials=credentials,
properties=new_subscription.properties,
expires_at=new_subscription.expires_at,
)
# TODO: Trying to invoke update api of the plugin trigger provider
# FALLBACK: If the update api is not implemented,
# delete the previous subscription and create a new one
# Unsubscribe the previous subscription (external call, but we'll handle errors)
try:
TriggerManager.unsubscribe_trigger(
tenant_id=tenant_id,
user_id=user_id,
provider_id=provider_id,
subscription=subscription.to_entity(),
credentials=decrypted_credentials,
credential_type=credential_type,
)
except Exception as e:
logger.exception("Error unsubscribing trigger during rebuild", exc_info=e)
# Continue anyway - the subscription might already be deleted externally
# Create a new subscription with the same subscription_id and endpoint_id (external call)
new_subscription: TriggerSubscriptionEntity = TriggerManager.subscribe_trigger(
tenant_id=tenant_id,
user_id=user_id,
provider_id=provider_id,
endpoint=generate_plugin_trigger_endpoint_url(subscription.endpoint_id),
parameters=parameters,
credentials=merged_credentials,
credential_type=credential_type,
)
# Update the subscription in the same transaction
# Inline update logic to reuse the same session
if name is not None and name != subscription.name:
existing = (
session.query(TriggerSubscription)
.filter_by(tenant_id=tenant_id, provider_id=str(provider_id), name=name)
.first()
)
if existing and existing.id != subscription.id:
raise ValueError(f"Subscription name '{name}' already exists for this provider")
subscription.name = name
# Update parameters
subscription.parameters = dict(parameters)
# Update credentials with merged (and encrypted) values
subscription.credentials = dict(credential_encrypter.encrypt(merged_credentials))
# Update properties
if new_subscription.properties:
properties_encrypter, _ = create_provider_encrypter(
tenant_id=tenant_id,
config=provider_controller.get_properties_schema(),
cache=NoOpProviderCredentialCache(),
)
subscription.properties = dict(properties_encrypter.encrypt(dict(new_subscription.properties)))
# Update expiration timestamp
if new_subscription.expires_at is not None:
subscription.expires_at = new_subscription.expires_at
# Commit the transaction
session.commit()
# Clear subscription cache
delete_cache_for_subscription(
tenant_id=tenant_id,
provider_id=subscription.provider_id,
subscription_id=subscription.id,
)
except Exception as e:
# Rollback on any error
session.rollback()
logger.exception("Failed to rebuild trigger subscription", exc_info=e)
raise

View File

@ -863,10 +863,18 @@ class WebhookService:
not_found_in_cache.append(node_id)
continue
with Session(db.engine) as session:
try:
# lock the concurrent webhook trigger creation
redis_client.lock(f"{cls.__WEBHOOK_NODE_CACHE_KEY__}:apps:{app.id}:lock", timeout=10)
lock_key = f"{cls.__WEBHOOK_NODE_CACHE_KEY__}:apps:{app.id}:lock"
lock = redis_client.lock(lock_key, timeout=10)
lock_acquired = False
try:
# acquire the lock with blocking and timeout
lock_acquired = lock.acquire(blocking=True, blocking_timeout=10)
if not lock_acquired:
logger.warning("Failed to acquire lock for webhook sync, app %s", app.id)
raise RuntimeError("Failed to acquire lock for webhook trigger synchronization")
with Session(db.engine) as session:
# fetch the non-cached nodes from DB
all_records = session.scalars(
select(WorkflowWebhookTrigger).where(
@ -903,11 +911,16 @@ class WebhookService:
session.delete(nodes_id_in_db[node_id])
redis_client.delete(f"{cls.__WEBHOOK_NODE_CACHE_KEY__}:{app.id}:{node_id}")
session.commit()
except Exception:
logger.exception("Failed to sync webhook relationships for app %s", app.id)
raise
finally:
redis_client.delete(f"{cls.__WEBHOOK_NODE_CACHE_KEY__}:apps:{app.id}:lock")
except Exception:
logger.exception("Failed to sync webhook relationships for app %s", app.id)
raise
finally:
# release the lock only if it was acquired
if lock_acquired:
try:
lock.release()
except Exception:
logger.exception("Failed to release lock for webhook sync, app %s", app.id)
@classmethod
def generate_webhook_id(cls) -> str:

View File

@ -0,0 +1,682 @@
from unittest.mock import MagicMock, patch
import pytest
from faker import Faker
from constants import HIDDEN_VALUE, UNKNOWN_VALUE
from core.plugin.entities.plugin_daemon import CredentialType
from core.trigger.entities.entities import Subscription as TriggerSubscriptionEntity
from extensions.ext_database import db
from models.provider_ids import TriggerProviderID
from models.trigger import TriggerSubscription
from services.trigger.trigger_provider_service import TriggerProviderService
class TestTriggerProviderService:
"""Integration tests for TriggerProviderService using testcontainers."""
@pytest.fixture
def mock_external_service_dependencies(self):
"""Mock setup for external service dependencies."""
with (
patch("services.trigger.trigger_provider_service.TriggerManager") as mock_trigger_manager,
patch("services.trigger.trigger_provider_service.redis_client") as mock_redis_client,
patch("services.trigger.trigger_provider_service.delete_cache_for_subscription") as mock_delete_cache,
patch("services.account_service.FeatureService") as mock_account_feature_service,
):
# Setup default mock returns
mock_provider_controller = MagicMock()
mock_provider_controller.get_credential_schema_config.return_value = MagicMock()
mock_provider_controller.get_properties_schema.return_value = MagicMock()
mock_trigger_manager.get_trigger_provider.return_value = mock_provider_controller
# Mock redis lock
mock_lock = MagicMock()
mock_lock.__enter__ = MagicMock(return_value=None)
mock_lock.__exit__ = MagicMock(return_value=None)
mock_redis_client.lock.return_value = mock_lock
# Setup account feature service mock
mock_account_feature_service.get_system_features.return_value.is_allow_register = True
yield {
"trigger_manager": mock_trigger_manager,
"redis_client": mock_redis_client,
"delete_cache": mock_delete_cache,
"provider_controller": mock_provider_controller,
"account_feature_service": mock_account_feature_service,
}
def _create_test_account_and_tenant(self, db_session_with_containers, mock_external_service_dependencies):
"""
Helper method to create a test account and tenant for testing.
Args:
db_session_with_containers: Database session from testcontainers infrastructure
mock_external_service_dependencies: Mock dependencies
Returns:
tuple: (account, tenant) - Created account and tenant instances
"""
fake = Faker()
from services.account_service import AccountService, TenantService
# Setup mocks for account creation
mock_external_service_dependencies[
"account_feature_service"
].get_system_features.return_value.is_allow_register = True
mock_external_service_dependencies[
"trigger_manager"
].get_trigger_provider.return_value = mock_external_service_dependencies["provider_controller"]
# Create account and tenant
account = AccountService.create_account(
email=fake.email(),
name=fake.name(),
interface_language="en-US",
password=fake.password(length=12),
)
TenantService.create_owner_tenant_if_not_exist(account, name=fake.company())
tenant = account.current_tenant
return account, tenant
def _create_test_subscription(
self,
db_session_with_containers,
tenant_id,
user_id,
provider_id,
credential_type,
credentials,
mock_external_service_dependencies,
):
"""
Helper method to create a test trigger subscription.
Args:
db_session_with_containers: Database session
tenant_id: Tenant ID
user_id: User ID
provider_id: Provider ID
credential_type: Credential type
credentials: Credentials dict
mock_external_service_dependencies: Mock dependencies
Returns:
TriggerSubscription: Created subscription instance
"""
fake = Faker()
from core.helper.provider_cache import NoOpProviderCredentialCache
from core.helper.provider_encryption import create_provider_encrypter
# Use mock provider controller to encrypt credentials
provider_controller = mock_external_service_dependencies["provider_controller"]
# Create encrypter for credentials
credential_encrypter, _ = create_provider_encrypter(
tenant_id=tenant_id,
config=provider_controller.get_credential_schema_config(credential_type),
cache=NoOpProviderCredentialCache(),
)
subscription = TriggerSubscription(
name=fake.word(),
tenant_id=tenant_id,
user_id=user_id,
provider_id=str(provider_id),
endpoint_id=fake.uuid4(),
parameters={"param1": "value1"},
properties={"prop1": "value1"},
credentials=dict(credential_encrypter.encrypt(credentials)),
credential_type=credential_type.value,
credential_expires_at=-1,
expires_at=-1,
)
db.session.add(subscription)
db.session.commit()
db.session.refresh(subscription)
return subscription
def test_rebuild_trigger_subscription_success_with_merged_credentials(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test successful rebuild with credential merging (HIDDEN_VALUE handling).
This test verifies:
- Credentials are properly merged (HIDDEN_VALUE replaced with existing values)
- Single transaction wraps all operations
- Merged credentials are used for subscribe and update
- Database state is correctly updated
"""
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
credential_type = CredentialType.API_KEY
# Create initial subscription with credentials
original_credentials = {"api_key": "original-secret-key", "api_secret": "original-secret"}
subscription = self._create_test_subscription(
db_session_with_containers,
tenant.id,
account.id,
provider_id,
credential_type,
original_credentials,
mock_external_service_dependencies,
)
# Prepare new credentials with HIDDEN_VALUE for api_key (should keep original)
# and new value for api_secret (should update)
new_credentials = {
"api_key": HIDDEN_VALUE, # Should be replaced with original
"api_secret": "new-secret-value", # Should be updated
}
# Mock subscribe_trigger to return a new subscription entity
new_subscription_entity = TriggerSubscriptionEntity(
endpoint=subscription.endpoint_id,
parameters={"param1": "value1"},
properties={"prop1": "new_prop_value"},
expires_at=1234567890,
)
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
# Mock unsubscribe_trigger
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
# Execute rebuild
TriggerProviderService.rebuild_trigger_subscription(
tenant_id=tenant.id,
provider_id=provider_id,
subscription_id=subscription.id,
credentials=new_credentials,
parameters={"param1": "updated_value"},
name="updated_name",
)
# Verify unsubscribe was called with decrypted original credentials
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.assert_called_once()
unsubscribe_call_args = mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.call_args
assert unsubscribe_call_args.kwargs["tenant_id"] == tenant.id
assert unsubscribe_call_args.kwargs["provider_id"] == provider_id
assert unsubscribe_call_args.kwargs["credential_type"] == credential_type
# Verify subscribe was called with merged credentials (api_key from original, api_secret new)
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.assert_called_once()
subscribe_call_args = mock_external_service_dependencies["trigger_manager"].subscribe_trigger.call_args
subscribe_credentials = subscribe_call_args.kwargs["credentials"]
assert subscribe_credentials["api_key"] == original_credentials["api_key"] # Merged from original
assert subscribe_credentials["api_secret"] == "new-secret-value" # New value
# Verify database state was updated
db.session.refresh(subscription)
assert subscription.name == "updated_name"
assert subscription.parameters == {"param1": "updated_value"}
# Verify credentials in DB were updated with merged values (decrypt to check)
from core.helper.provider_cache import NoOpProviderCredentialCache
from core.helper.provider_encryption import create_provider_encrypter
# Use mock provider controller to decrypt credentials
provider_controller = mock_external_service_dependencies["provider_controller"]
credential_encrypter, _ = create_provider_encrypter(
tenant_id=tenant.id,
config=provider_controller.get_credential_schema_config(credential_type),
cache=NoOpProviderCredentialCache(),
)
decrypted_db_credentials = dict(credential_encrypter.decrypt(subscription.credentials))
assert decrypted_db_credentials["api_key"] == original_credentials["api_key"]
assert decrypted_db_credentials["api_secret"] == "new-secret-value"
# Verify cache was cleared
mock_external_service_dependencies["delete_cache"].assert_called_once_with(
tenant_id=tenant.id,
provider_id=subscription.provider_id,
subscription_id=subscription.id,
)
def test_rebuild_trigger_subscription_with_all_new_credentials(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test rebuild when all credentials are new (no HIDDEN_VALUE).
This test verifies:
- All new credentials are used when no HIDDEN_VALUE is present
- Merged credentials contain only new values
"""
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
credential_type = CredentialType.API_KEY
# Create initial subscription
original_credentials = {"api_key": "original-key", "api_secret": "original-secret"}
subscription = self._create_test_subscription(
db_session_with_containers,
tenant.id,
account.id,
provider_id,
credential_type,
original_credentials,
mock_external_service_dependencies,
)
# All new credentials (no HIDDEN_VALUE)
new_credentials = {
"api_key": "completely-new-key",
"api_secret": "completely-new-secret",
}
new_subscription_entity = TriggerSubscriptionEntity(
endpoint=subscription.endpoint_id,
parameters={},
properties={},
expires_at=-1,
)
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
# Execute rebuild
TriggerProviderService.rebuild_trigger_subscription(
tenant_id=tenant.id,
provider_id=provider_id,
subscription_id=subscription.id,
credentials=new_credentials,
parameters={},
)
# Verify subscribe was called with all new credentials
subscribe_call_args = mock_external_service_dependencies["trigger_manager"].subscribe_trigger.call_args
subscribe_credentials = subscribe_call_args.kwargs["credentials"]
assert subscribe_credentials["api_key"] == "completely-new-key"
assert subscribe_credentials["api_secret"] == "completely-new-secret"
def test_rebuild_trigger_subscription_with_all_hidden_values(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test rebuild when all credentials are HIDDEN_VALUE (preserve all existing).
This test verifies:
- All HIDDEN_VALUE credentials are replaced with existing values
- Original credentials are preserved
"""
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
credential_type = CredentialType.API_KEY
original_credentials = {"api_key": "original-key", "api_secret": "original-secret"}
subscription = self._create_test_subscription(
db_session_with_containers,
tenant.id,
account.id,
provider_id,
credential_type,
original_credentials,
mock_external_service_dependencies,
)
# All HIDDEN_VALUE (should preserve all original)
new_credentials = {
"api_key": HIDDEN_VALUE,
"api_secret": HIDDEN_VALUE,
}
new_subscription_entity = TriggerSubscriptionEntity(
endpoint=subscription.endpoint_id,
parameters={},
properties={},
expires_at=-1,
)
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
# Execute rebuild
TriggerProviderService.rebuild_trigger_subscription(
tenant_id=tenant.id,
provider_id=provider_id,
subscription_id=subscription.id,
credentials=new_credentials,
parameters={},
)
# Verify subscribe was called with all original credentials
subscribe_call_args = mock_external_service_dependencies["trigger_manager"].subscribe_trigger.call_args
subscribe_credentials = subscribe_call_args.kwargs["credentials"]
assert subscribe_credentials["api_key"] == original_credentials["api_key"]
assert subscribe_credentials["api_secret"] == original_credentials["api_secret"]
def test_rebuild_trigger_subscription_with_missing_key_uses_unknown_value(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test rebuild when HIDDEN_VALUE is used for a key that doesn't exist in original.
This test verifies:
- UNKNOWN_VALUE is used when HIDDEN_VALUE key doesn't exist in original credentials
"""
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
credential_type = CredentialType.API_KEY
# Original has only api_key
original_credentials = {"api_key": "original-key"}
subscription = self._create_test_subscription(
db_session_with_containers,
tenant.id,
account.id,
provider_id,
credential_type,
original_credentials,
mock_external_service_dependencies,
)
# HIDDEN_VALUE for non-existent key should use UNKNOWN_VALUE
new_credentials = {
"api_key": HIDDEN_VALUE,
"non_existent_key": HIDDEN_VALUE, # This key doesn't exist in original
}
new_subscription_entity = TriggerSubscriptionEntity(
endpoint=subscription.endpoint_id,
parameters={},
properties={},
expires_at=-1,
)
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
# Execute rebuild
TriggerProviderService.rebuild_trigger_subscription(
tenant_id=tenant.id,
provider_id=provider_id,
subscription_id=subscription.id,
credentials=new_credentials,
parameters={},
)
# Verify subscribe was called with original api_key and UNKNOWN_VALUE for missing key
subscribe_call_args = mock_external_service_dependencies["trigger_manager"].subscribe_trigger.call_args
subscribe_credentials = subscribe_call_args.kwargs["credentials"]
assert subscribe_credentials["api_key"] == original_credentials["api_key"]
assert subscribe_credentials["non_existent_key"] == UNKNOWN_VALUE
def test_rebuild_trigger_subscription_rollback_on_error(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test that transaction is rolled back on error.
This test verifies:
- Database transaction is rolled back when an error occurs
- Original subscription state is preserved
"""
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
credential_type = CredentialType.API_KEY
original_credentials = {"api_key": "original-key"}
subscription = self._create_test_subscription(
db_session_with_containers,
tenant.id,
account.id,
provider_id,
credential_type,
original_credentials,
mock_external_service_dependencies,
)
original_name = subscription.name
original_parameters = subscription.parameters.copy()
# Make subscribe_trigger raise an error
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.side_effect = ValueError(
"Subscribe failed"
)
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
# Execute rebuild and expect error
with pytest.raises(ValueError, match="Subscribe failed"):
TriggerProviderService.rebuild_trigger_subscription(
tenant_id=tenant.id,
provider_id=provider_id,
subscription_id=subscription.id,
credentials={"api_key": "new-key"},
parameters={},
)
# Verify subscription state was not changed (rolled back)
db.session.refresh(subscription)
assert subscription.name == original_name
assert subscription.parameters == original_parameters
def test_rebuild_trigger_subscription_unsubscribe_error_continues(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test that unsubscribe errors are handled gracefully and operation continues.
This test verifies:
- Unsubscribe errors are caught and logged but don't stop the rebuild
- Rebuild continues even if unsubscribe fails
"""
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
credential_type = CredentialType.API_KEY
original_credentials = {"api_key": "original-key"}
subscription = self._create_test_subscription(
db_session_with_containers,
tenant.id,
account.id,
provider_id,
credential_type,
original_credentials,
mock_external_service_dependencies,
)
# Make unsubscribe_trigger raise an error (should be caught and continue)
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.side_effect = ValueError(
"Unsubscribe failed"
)
new_subscription_entity = TriggerSubscriptionEntity(
endpoint=subscription.endpoint_id,
parameters={},
properties={},
expires_at=-1,
)
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
# Execute rebuild - should succeed despite unsubscribe error
TriggerProviderService.rebuild_trigger_subscription(
tenant_id=tenant.id,
provider_id=provider_id,
subscription_id=subscription.id,
credentials={"api_key": "new-key"},
parameters={},
)
# Verify subscribe was still called (operation continued)
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.assert_called_once()
# Verify subscription was updated
db.session.refresh(subscription)
assert subscription.parameters == {}
def test_rebuild_trigger_subscription_subscription_not_found(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test error when subscription is not found.
This test verifies:
- Proper error is raised when subscription doesn't exist
"""
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
fake_subscription_id = fake.uuid4()
with pytest.raises(ValueError, match="not found"):
TriggerProviderService.rebuild_trigger_subscription(
tenant_id=tenant.id,
provider_id=provider_id,
subscription_id=fake_subscription_id,
credentials={},
parameters={},
)
def test_rebuild_trigger_subscription_provider_not_found(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test error when provider is not found.
This test verifies:
- Proper error is raised when provider doesn't exist
"""
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_id = TriggerProviderID("non_existent_org/non_existent_plugin/non_existent_provider")
# Make get_trigger_provider return None
mock_external_service_dependencies["trigger_manager"].get_trigger_provider.return_value = None
with pytest.raises(ValueError, match="Provider.*not found"):
TriggerProviderService.rebuild_trigger_subscription(
tenant_id=tenant.id,
provider_id=provider_id,
subscription_id=fake.uuid4(),
credentials={},
parameters={},
)
def test_rebuild_trigger_subscription_unsupported_credential_type(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test error when credential type is not supported for rebuild.
This test verifies:
- Proper error is raised for unsupported credential types (not OAUTH2 or API_KEY)
"""
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
credential_type = CredentialType.UNAUTHORIZED # Not supported
subscription = self._create_test_subscription(
db_session_with_containers,
tenant.id,
account.id,
provider_id,
credential_type,
{},
mock_external_service_dependencies,
)
with pytest.raises(ValueError, match="Credential type not supported for rebuild"):
TriggerProviderService.rebuild_trigger_subscription(
tenant_id=tenant.id,
provider_id=provider_id,
subscription_id=subscription.id,
credentials={},
parameters={},
)
def test_rebuild_trigger_subscription_name_uniqueness_check(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test that name uniqueness is checked when updating name.
This test verifies:
- Error is raised when new name conflicts with existing subscription
"""
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
credential_type = CredentialType.API_KEY
# Create first subscription
subscription1 = self._create_test_subscription(
db_session_with_containers,
tenant.id,
account.id,
provider_id,
credential_type,
{"api_key": "key1"},
mock_external_service_dependencies,
)
# Create second subscription with different name
subscription2 = self._create_test_subscription(
db_session_with_containers,
tenant.id,
account.id,
provider_id,
credential_type,
{"api_key": "key2"},
mock_external_service_dependencies,
)
new_subscription_entity = TriggerSubscriptionEntity(
endpoint=subscription2.endpoint_id,
parameters={},
properties={},
expires_at=-1,
)
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
# Try to rename subscription2 to subscription1's name (should fail)
with pytest.raises(ValueError, match="already exists"):
TriggerProviderService.rebuild_trigger_subscription(
tenant_id=tenant.id,
provider_id=provider_id,
subscription_id=subscription2.id,
credentials={"api_key": "new-key"},
parameters={},
name=subscription1.name, # Conflicting name
)

View File

@ -3072,11 +3072,11 @@ wheels = [
[[package]]
name = "json-repair"
version = "0.54.1"
version = "0.54.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/00/46/d3a4d9a3dad39bb4a2ad16b8adb9fe2e8611b20b71197fe33daa6768e85d/json_repair-0.54.1.tar.gz", hash = "sha256:d010bc31f1fc66e7c36dc33bff5f8902674498ae5cb8e801ad455a53b455ad1d", size = 38555, upload-time = "2025-11-19T14:55:24.265Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b5/86/48b12ac02032f121ac7e5f11a32143edca6c1e3d19ffc54d6fb9ca0aafd0/json_repair-0.54.3.tar.gz", hash = "sha256:e50feec9725e52ac91f12184609754684ac1656119dfbd31de09bdaf9a1d8bf6", size = 38626, upload-time = "2025-12-15T09:41:58.594Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/96/c9aad7ee949cc1bf15df91f347fbc2d3bd10b30b80c7df689ce6fe9332b5/json_repair-0.54.1-py3-none-any.whl", hash = "sha256:016160c5db5d5fe443164927bb58d2dfbba5f43ad85719fa9bc51c713a443ab1", size = 29311, upload-time = "2025-11-19T14:55:22.886Z" },
{ url = "https://files.pythonhosted.org/packages/e9/08/abe317237add63c3e62f18a981bccf92112b431835b43d844aedaf61f4a0/json_repair-0.54.3-py3-none-any.whl", hash = "sha256:4cdc132ee27d4780576f71bf27a113877046224a808bfc17392e079cb344fb81", size = 29357, upload-time = "2025-12-15T09:41:57.436Z" },
]
[[package]]

View File

@ -34,7 +34,6 @@ import Records from './components/records'
import ResultItem from './components/result-item'
import ResultItemExternal from './components/result-item-external'
import ModifyRetrievalModal from './modify-retrieval-modal'
import s from './style.module.css'
const limit = 10
@ -115,8 +114,8 @@ const HitTestingPage: FC<Props> = ({ datasetId }: Props) => {
}, [isMobile, setShowRightPanel])
return (
<div className={s.container}>
<div className="flex flex-col px-6 py-3">
<div className="relative flex h-full w-full gap-x-6 overflow-y-auto pl-6">
<div className="flex min-w-0 flex-1 flex-col py-3">
<div className="mb-4 flex flex-col justify-center">
<h1 className="text-base font-semibold text-text-primary">{t('datasetHitTesting.title')}</h1>
<p className="mt-0.5 text-[13px] font-normal leading-4 text-text-tertiary">{t('datasetHitTesting.desc')}</p>
@ -161,7 +160,7 @@ const HitTestingPage: FC<Props> = ({ datasetId }: Props) => {
onClose={hideRightPanel}
footer={null}
>
<div className="flex flex-col pt-3">
<div className="flex min-w-0 flex-1 flex-col pt-3">
{isRetrievalLoading
? (
<div className="flex h-full flex-col rounded-tl-2xl bg-background-body px-4 py-3">

View File

@ -1,43 +0,0 @@
.container {
@apply flex h-full w-full relative overflow-y-auto;
}
.container>div {
@apply flex-1 h-full;
}
.commonIcon {
@apply w-3.5 h-3.5 inline-block align-middle;
background-repeat: no-repeat;
background-position: center center;
background-size: contain;
}
.app_icon {
background-image: url(./assets/grid.svg);
}
.hit_testing_icon {
background-image: url(../documents/assets/target.svg);
}
.plugin_icon {
background-image: url(./assets/plugin.svg);
}
.cardWrapper {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(284px, auto));
grid-gap: 16px;
grid-auto-rows: 216px;
}
.clockWrapper {
border: 0.5px solid #eaecf5;
@apply rounded-lg w-11 h-11 flex justify-center items-center;
}
.clockIcon {
mask-image: url(./assets/clock.svg);
@apply bg-gray-500;
}

View File

@ -0,0 +1,23 @@
'use client'
import { TanStackDevtools } from '@tanstack/react-devtools'
import { formDevtoolsPlugin } from '@tanstack/react-form-devtools'
import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
import * as React from 'react'
export function TanStackDevtoolsWrapper() {
return (
<TanStackDevtools
plugins={[
// Query Devtools (Official Plugin)
{
name: 'React Query',
render: () => <ReactQueryDevtoolsPanel />,
},
// Form Devtools (Official Plugin)
formDevtoolsPlugin(),
]}
/>
)
}

View File

@ -3,14 +3,14 @@
import * as Sentry from '@sentry/react'
import { useEffect } from 'react'
const isDevelopment = process.env.NODE_ENV === 'development'
import { IS_DEV } from '@/config'
const SentryInitializer = ({
children,
}: { children: React.ReactElement }) => {
useEffect(() => {
const SENTRY_DSN = document?.body?.getAttribute('data-public-sentry-dsn')
if (!isDevelopment && SENTRY_DSN) {
if (!IS_DEV && SENTRY_DSN) {
Sentry.init({
dsn: SENTRY_DSN,
integrations: [

View File

@ -48,6 +48,7 @@ const EditCustomCollectionModal: FC<Props> = ({
const [editFirst, setEditFirst] = useState(!isAdd)
const [paramsSchemas, setParamsSchemas] = useState<CustomParamSchema[]>(payload?.tools || [])
const [labels, setLabels] = useState<string[]>(payload?.labels || [])
const [customCollection, setCustomCollection, getCustomCollection] = useGetState<CustomCollectionBackend>(isAdd
? {
provider: '',
@ -67,6 +68,15 @@ const EditCustomCollectionModal: FC<Props> = ({
const originalProvider = isEdit ? payload.provider : ''
// Sync customCollection state when payload changes
useEffect(() => {
if (isEdit) {
setCustomCollection(payload)
setParamsSchemas(payload.tools || [])
setLabels(payload.labels || [])
}
}, [isEdit, payload])
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const emoji = customCollection.icon
const setEmoji = (emoji: Emoji) => {
@ -124,7 +134,6 @@ const EditCustomCollectionModal: FC<Props> = ({
const [currTool, setCurrTool] = useState<CustomParamSchema | null>(null)
const [isShowTestApi, setIsShowTestApi] = useState(false)
const [labels, setLabels] = useState<string[]>(payload?.labels || [])
const handleLabelSelect = (value: string[]) => {
setLabels(value)
}

View File

@ -100,9 +100,28 @@ const ProviderDetail = ({
const [isShowEditCollectionToolModal, setIsShowEditCustomCollectionModal] = useState(false)
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [deleteAction, setDeleteAction] = useState('')
const getCustomProvider = useCallback(async () => {
setIsDetailLoading(true)
const res = await fetchCustomCollection(collection.name)
if (res.credentials.auth_type === AuthType.apiKey && !res.credentials.api_key_header_prefix) {
if (res.credentials.api_key_value)
res.credentials.api_key_header_prefix = AuthHeaderPrefix.custom
}
setCustomCollection({
...res,
labels: collection.labels,
provider: collection.name,
})
setIsDetailLoading(false)
}, [collection.labels, collection.name])
const doUpdateCustomToolCollection = async (data: CustomCollectionBackend) => {
await updateCustomCollection(data)
onRefreshData()
await getCustomProvider()
// Use fresh data from form submission to avoid race condition with collection.labels
setCustomCollection(prev => prev ? { ...prev, labels: data.labels } : null)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
@ -118,20 +137,6 @@ const ProviderDetail = ({
})
setIsShowEditCustomCollectionModal(false)
}
const getCustomProvider = useCallback(async () => {
setIsDetailLoading(true)
const res = await fetchCustomCollection(collection.name)
if (res.credentials.auth_type === AuthType.apiKey && !res.credentials.api_key_header_prefix) {
if (res.credentials.api_key_value)
res.credentials.api_key_header_prefix = AuthHeaderPrefix.custom
}
setCustomCollection({
...res,
labels: collection.labels,
provider: collection.name,
})
setIsDetailLoading(false)
}, [collection.labels, collection.name])
// workflow provider
const [isShowEditWorkflowToolModal, setIsShowEditWorkflowToolModal] = useState(false)
const getWorkflowToolProvider = useCallback(async () => {

View File

@ -61,7 +61,7 @@ const VersionHistoryButton: FC<VersionHistoryButtonProps> = ({
>
<Button
className={cn(
'p-2 rounded-lg border border-transparent',
'rounded-lg border border-transparent p-2',
theme === 'dark' && 'border-black/5 bg-white/10 backdrop-blur-sm',
)}
onClick={handleViewVersionHistory}

View File

@ -35,6 +35,7 @@ import ReactFlow, {
useReactFlow,
useStoreApi,
} from 'reactflow'
import { IS_DEV } from '@/config'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import {
useAllBuiltInTools,
@ -361,7 +362,7 @@ export const Workflow: FC<WorkflowProps> = memo(({
}
}, [schemaTypeDefinitions, fetchInspectVars, isLoadedVars, vars, customTools, buildInTools, workflowTools, mcpTools, dataSourceList])
if (process.env.NODE_ENV === 'development') {
if (IS_DEV) {
store.getState().onError = (code, message) => {
if (code === '002')
return

View File

@ -1,68 +0,0 @@
'use client'
import BaseForm from '../components/base/form/form-scenarios/base'
import { BaseFieldType } from '../components/base/form/form-scenarios/base/types'
export default function Page() {
return (
<div className="flex h-screen w-full items-center justify-center p-20">
<div className="w-[400px] rounded-lg border border-components-panel-border bg-components-panel-bg">
<BaseForm
initialData={{
type: 'option_1',
variable: 'test',
label: 'Test',
maxLength: 48,
required: true,
}}
configurations={[
{
type: BaseFieldType.textInput,
variable: 'variable',
label: 'Variable',
required: true,
showConditions: [],
},
{
type: BaseFieldType.textInput,
variable: 'label',
label: 'Label',
required: true,
showConditions: [],
},
{
type: BaseFieldType.numberInput,
variable: 'maxLength',
label: 'Max Length',
required: true,
showConditions: [],
max: 100,
min: 1,
},
{
type: BaseFieldType.checkbox,
variable: 'required',
label: 'Required',
required: true,
showConditions: [],
},
{
type: BaseFieldType.select,
variable: 'type',
label: 'Type',
required: true,
showConditions: [],
options: [
{ label: 'Option 1', value: 'option_1' },
{ label: 'Option 2', value: 'option_2' },
{ label: 'Option 3', value: 'option_3' },
],
},
]}
onSubmit={(value) => {
console.log('onSubmit', value)
}}
/>
</div>
</div>
)
}

View File

@ -1,6 +1,8 @@
import type { Viewport } from 'next'
import { ThemeProvider } from 'next-themes'
import dynamic from 'next/dynamic'
import { Instrument_Serif } from 'next/font/google'
import { IS_DEV } from '@/config'
import GlobalPublicStoreProvider from '@/context/global-public-context'
import { TanstackQueryInitializer } from '@/context/query-client'
import { getLocaleOnServer } from '@/i18n-config/server'
@ -8,12 +10,15 @@ import { DatasetAttr } from '@/types/feature'
import { cn } from '@/utils/classnames'
import BrowserInitializer from './components/browser-initializer'
import I18nServer from './components/i18n-server'
import { ReactScan } from './components/react-scan'
import SentryInitializer from './components/sentry-initializer'
import RoutePrefixHandle from './routePrefixHandle'
import './styles/globals.css'
import './styles/markdown.scss'
const ReactScan = IS_DEV
? dynamic(() => import('./components/react-scan').then(m => m.ReactScan), { ssr: false })
: () => null
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,

View File

@ -2,7 +2,14 @@
import type { FC, PropsWithChildren } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { lazy, Suspense } from 'react'
import { IS_DEV } from '@/config'
const TanStackDevtoolsWrapper = lazy(() =>
import('@/app/components/devtools').then(module => ({
default: module.TanStackDevtoolsWrapper,
})),
)
const STALE_TIME = 1000 * 60 * 30 // 30 minutes
@ -19,7 +26,11 @@ export const TanstackQueryInitializer: FC<PropsWithChildren> = (props) => {
return (
<QueryClientProvider client={client}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
{IS_DEV && (
<Suspense fallback={null}>
<TanStackDevtoolsWrapper />
</Suspense>
)}
</QueryClientProvider>
)
}

View File

@ -3,7 +3,7 @@
"type": "module",
"version": "1.11.2",
"private": true,
"packageManager": "pnpm@10.26.1+sha512.664074abc367d2c9324fdc18037097ce0a8f126034160f709928e9e9f95d98714347044e5c3164d65bd5da6c59c6be362b107546292a8eecb7999196e5ce58fa",
"packageManager": "pnpm@10.26.2+sha512.0e308ff2005fc7410366f154f625f6631ab2b16b1d2e70238444dd6ae9d630a8482d92a451144debc492416896ed16f7b114a86ec68b8404b2443869e68ffda6",
"engines": {
"node": ">=v22.11.0"
},
@ -71,7 +71,6 @@
"@tailwindcss/typography": "^0.5.19",
"@tanstack/react-form": "^1.23.7",
"@tanstack/react-query": "^5.90.5",
"@tanstack/react-query-devtools": "^5.90.2",
"abcjs": "^6.5.2",
"ahooks": "^3.9.5",
"class-variance-authority": "^0.7.1",
@ -134,7 +133,7 @@
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"scheduler": "^0.26.0",
"scheduler": "^0.27.0",
"semver": "^7.7.3",
"sharp": "^0.33.5",
"sortablejs": "^1.15.6",
@ -164,6 +163,9 @@
"@storybook/addon-themes": "9.1.13",
"@storybook/nextjs": "9.1.13",
"@storybook/react": "9.1.13",
"@tanstack/react-devtools": "^0.9.0",
"@tanstack/react-form-devtools": "^0.2.9",
"@tanstack/react-query-devtools": "^5.90.2",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",

View File

@ -129,9 +129,6 @@ importers:
'@tanstack/react-query':
specifier: ^5.90.5
version: 5.90.12(react@19.2.3)
'@tanstack/react-query-devtools':
specifier: ^5.90.2
version: 5.91.1(@tanstack/react-query@5.90.12(react@19.2.3))(react@19.2.3)
abcjs:
specifier: ^6.5.2
version: 6.5.2
@ -319,8 +316,8 @@ importers:
specifier: ^6.0.0
version: 6.0.0
scheduler:
specifier: ^0.26.0
version: 0.26.0
specifier: ^0.27.0
version: 0.27.0
semver:
specifier: ^7.7.3
version: 7.7.3
@ -341,7 +338,7 @@ importers:
version: 7.0.19
use-context-selector:
specifier: ^2.0.0
version: 2.0.0(react@19.2.3)(scheduler@0.26.0)
version: 2.0.0(react@19.2.3)(scheduler@0.27.0)
uuid:
specifier: ^10.0.0
version: 10.0.0
@ -403,6 +400,15 @@ importers:
'@storybook/react':
specifier: 9.1.13
version: 9.1.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@18.15.0)(jiti@1.21.7)(sass@1.95.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(typescript@5.9.3)
'@tanstack/react-devtools':
specifier: ^0.9.0
version: 0.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10)
'@tanstack/react-form-devtools':
specifier: ^0.2.9
version: 0.2.9(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10)
'@tanstack/react-query-devtools':
specifier: ^5.90.2
version: 5.91.1(@tanstack/react-query@5.90.12(react@19.2.3))(react@19.2.3)
'@testing-library/dom':
specifier: ^10.4.1
version: 10.4.1
@ -3169,6 +3175,36 @@ packages:
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
engines: {node: '>=10'}
'@solid-primitives/event-listener@2.4.3':
resolution: {integrity: sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg==}
peerDependencies:
solid-js: ^1.6.12
'@solid-primitives/keyboard@1.3.3':
resolution: {integrity: sha512-9dQHTTgLBqyAI7aavtO+HnpTVJgWQA1ghBSrmLtMu1SMxLPDuLfuNr+Tk5udb4AL4Ojg7h9JrKOGEEDqsJXWJA==}
peerDependencies:
solid-js: ^1.6.12
'@solid-primitives/resize-observer@2.1.3':
resolution: {integrity: sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ==}
peerDependencies:
solid-js: ^1.6.12
'@solid-primitives/rootless@1.5.2':
resolution: {integrity: sha512-9HULb0QAzL2r47CCad0M+NKFtQ+LrGGNHZfteX/ThdGvKIg2o2GYhBooZubTCd/RTu2l2+Nw4s+dEfiDGvdrrQ==}
peerDependencies:
solid-js: ^1.6.12
'@solid-primitives/static-store@0.1.2':
resolution: {integrity: sha512-ReK+5O38lJ7fT+L6mUFvUr6igFwHBESZF+2Ug842s7fvlVeBdIVEdTCErygff6w7uR6+jrr7J8jQo+cYrEq4Iw==}
peerDependencies:
solid-js: ^1.6.12
'@solid-primitives/utils@6.3.2':
resolution: {integrity: sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ==}
peerDependencies:
solid-js: ^1.6.12
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
@ -3308,13 +3344,67 @@ packages:
peerDependencies:
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
'@tanstack/devtools-client@0.0.5':
resolution: {integrity: sha512-hsNDE3iu4frt9cC2ppn1mNRnLKo2uc1/1hXAyY9z4UYb+o40M2clFAhiFoo4HngjfGJDV3x18KVVIq7W4Un+zA==}
engines: {node: '>=18'}
'@tanstack/devtools-event-bus@0.4.0':
resolution: {integrity: sha512-1t+/csFuDzi+miDxAOh6Xv7VDE80gJEItkTcAZLjV5MRulbO/W8ocjHLI2Do/p2r2/FBU0eKCRTpdqvXaYoHpQ==}
engines: {node: '>=18'}
'@tanstack/devtools-event-client@0.3.5':
resolution: {integrity: sha512-RL1f5ZlfZMpghrCIdzl6mLOFLTuhqmPNblZgBaeKfdtk5rfbjykurv+VfYydOFXj0vxVIoA2d/zT7xfD7Ph8fw==}
engines: {node: '>=18'}
'@tanstack/devtools-event-client@0.4.0':
resolution: {integrity: sha512-RPfGuk2bDZgcu9bAJodvO2lnZeHuz4/71HjZ0bGb/SPg8+lyTA+RLSKQvo7fSmPSi8/vcH3aKQ8EM9ywf1olaw==}
engines: {node: '>=18'}
'@tanstack/devtools-ui@0.4.4':
resolution: {integrity: sha512-5xHXFyX3nom0UaNfiOM92o6ziaHjGo3mcSGe2HD5Xs8dWRZNpdZ0Smd0B9ddEhy0oB+gXyMzZgUJb9DmrZV0Mg==}
engines: {node: '>=18'}
peerDependencies:
solid-js: '>=1.9.7'
'@tanstack/devtools-utils@0.0.9':
resolution: {integrity: sha512-tCObM6wbEjuHeGNs3JDhrqBhoMxpJpVuVIg5Kc33EmUI1ZO7KLpC1277Qf6AmSWy3aVOreGwn3y5bJzxmAJNXg==}
engines: {node: '>=18'}
peerDependencies:
'@types/react': ~19.2.7
react: '>=17.0.0'
solid-js: '>=1.9.7'
vue: '>=3.2.0'
peerDependenciesMeta:
'@types/react':
optional: true
react:
optional: true
solid-js:
optional: true
vue:
optional: true
'@tanstack/devtools@0.10.1':
resolution: {integrity: sha512-1gtPmCDXV4Pl1nVtoqwjV0tc4E9GMuFtlkBX1Lz1KfqI3W9JojT5YsVifOQ/g8BTQ5w5+tyIANwHU7WYgLq/MQ==}
engines: {node: '>=18'}
peerDependencies:
solid-js: '>=1.9.7'
'@tanstack/form-core@1.27.1':
resolution: {integrity: sha512-hPM+0tUnZ2C2zb2TE1lar1JJ0S0cbnQHlUwFcCnVBpMV3rjtUzkoM766gUpWrlmTGCzNad0GbJ0aTxVsjT6J8g==}
'@tanstack/form-core@1.27.6':
resolution: {integrity: sha512-1C4PUpOcCpivddKxtAeqdeqncxnPKiPpTVDRknDExCba+6zCsAjxgL+p3qYA3hu+EFyUAdW71rU+uqYbEa7qqA==}
'@tanstack/form-devtools@0.2.9':
resolution: {integrity: sha512-KOJiwvlFPsHeuWXvHUXRVdciXG1OPhg1c476MsLre0YLdaw1jeMlDYSlqq7sdEULX+2Sg/lhNpX86QbQuxzd2A==}
peerDependencies:
solid-js: '>=1.9.9'
'@tanstack/pacer-lite@0.1.1':
resolution: {integrity: sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==}
engines: {node: '>=18'}
'@tanstack/pacer@0.15.4':
resolution: {integrity: sha512-vGY+CWsFZeac3dELgB6UZ4c7OacwsLb8hvL2gLS6hTgy8Fl0Bm/aLokHaeDIP+q9F9HUZTnp360z9uv78eg8pg==}
engines: {node: '>=18'}
@ -3325,6 +3415,20 @@ packages:
'@tanstack/query-devtools@5.91.1':
resolution: {integrity: sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg==}
'@tanstack/react-devtools@0.9.0':
resolution: {integrity: sha512-Lq0svXOTG5N61SHgx8F0on6zz2GB0kmFjN/yyfNLrJyRgJ+U3jYFRd9ti3uBPABsXzHQMHYYujnTXrOYp/OaUg==}
engines: {node: '>=18'}
peerDependencies:
'@types/react': ~19.2.7
'@types/react-dom': ~19.2.3
react: '>=16.8'
react-dom: '>=16.8'
'@tanstack/react-form-devtools@0.2.9':
resolution: {integrity: sha512-wg0xrcVY8evIFGVHrnl9s+/9ENzuVbqv5Ru4HyAJjjL4uECtl6KdDJsi0lZdOyoM1UYEQoVdcN8jfBbxkA3q1g==}
peerDependencies:
react: ^17.0.0 || ^18.0.0 || ^19.0.0
'@tanstack/react-form@1.27.1':
resolution: {integrity: sha512-HKP0Ew2ae9AL5vU1PkJ+oAC2p+xBtA905u0fiNLzlfn1vLkBxenfg5L6TOA+rZITHpQsSo10tqwc5Yw6qn8Mpg==}
peerDependencies:
@ -3594,6 +3698,9 @@ packages:
'@types/node@20.19.26':
resolution: {integrity: sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==}
'@types/node@20.19.27':
resolution: {integrity: sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==}
'@types/papaparse@5.5.1':
resolution: {integrity: sha512-esEO+VISsLIyE+JZBmb89NzsYYbpwV8lmv2rPo6oX5y9KhBaIP7hhHgjuTut54qjdKVMufTEcrh5fUl9+58huw==}
@ -5629,6 +5736,11 @@ packages:
globrex@0.1.2:
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
goober@2.1.18:
resolution: {integrity: sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==}
peerDependencies:
csstype: ^3.0.10
got@11.8.6:
resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==}
engines: {node: '>=10.19.0'}
@ -6203,8 +6315,8 @@ packages:
lexical@0.38.2:
resolution: {integrity: sha512-JJmfsG3c4gwBHzUGffbV7ifMNkKAWMCnYE3xJl87gty7hjyV5f3xq7eqTjP5HFYvO4XpjJvvWO2/djHp5S10tw==}
lib0@0.2.115:
resolution: {integrity: sha512-noaW4yNp6hCjOgDnWWxW0vGXE3kZQI5Kqiwz+jIWXavI9J9WyfJ9zjsbQlQlgjIbHBrvlA/x3TSIXBUJj+0L6g==}
lib0@0.2.116:
resolution: {integrity: sha512-4zsosjzmt33rx5XjmFVYUAeLNh+BTeDTiwGdLt4muxiir2btsc60Nal0EvkvDRizg+pnlK1q+BtYi7M+d4eStw==}
engines: {node: '>=16'}
hasBin: true
@ -7641,9 +7753,6 @@ packages:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
scheduler@0.26.0:
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
@ -7687,6 +7796,16 @@ packages:
serialize-javascript@6.0.2:
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
seroval-plugins@1.3.3:
resolution: {integrity: sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==}
engines: {node: '>=10'}
peerDependencies:
seroval: ^1.0
seroval@1.3.2:
resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==}
engines: {node: '>=10'}
setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
@ -7753,6 +7872,9 @@ packages:
resolution: {integrity: sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ==}
engines: {node: '>= 18'}
solid-js@1.9.10:
resolution: {integrity: sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==}
sortablejs@1.15.6:
resolution: {integrity: sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==}
@ -8589,6 +8711,7 @@ packages:
whatwg-encoding@3.1.1:
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
engines: {node: '>=18'}
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
whatwg-mimetype@3.0.0:
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
@ -11539,6 +11662,40 @@ snapshots:
'@sindresorhus/is@4.6.0': {}
'@solid-primitives/event-listener@2.4.3(solid-js@1.9.10)':
dependencies:
'@solid-primitives/utils': 6.3.2(solid-js@1.9.10)
solid-js: 1.9.10
'@solid-primitives/keyboard@1.3.3(solid-js@1.9.10)':
dependencies:
'@solid-primitives/event-listener': 2.4.3(solid-js@1.9.10)
'@solid-primitives/rootless': 1.5.2(solid-js@1.9.10)
'@solid-primitives/utils': 6.3.2(solid-js@1.9.10)
solid-js: 1.9.10
'@solid-primitives/resize-observer@2.1.3(solid-js@1.9.10)':
dependencies:
'@solid-primitives/event-listener': 2.4.3(solid-js@1.9.10)
'@solid-primitives/rootless': 1.5.2(solid-js@1.9.10)
'@solid-primitives/static-store': 0.1.2(solid-js@1.9.10)
'@solid-primitives/utils': 6.3.2(solid-js@1.9.10)
solid-js: 1.9.10
'@solid-primitives/rootless@1.5.2(solid-js@1.9.10)':
dependencies:
'@solid-primitives/utils': 6.3.2(solid-js@1.9.10)
solid-js: 1.9.10
'@solid-primitives/static-store@0.1.2(solid-js@1.9.10)':
dependencies:
'@solid-primitives/utils': 6.3.2(solid-js@1.9.10)
solid-js: 1.9.10
'@solid-primitives/utils@6.3.2(solid-js@1.9.10)':
dependencies:
solid-js: 1.9.10
'@standard-schema/spec@1.1.0': {}
'@standard-schema/utils@0.3.0': {}
@ -11766,14 +11923,84 @@ snapshots:
postcss-selector-parser: 6.0.10
tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.8.2)
'@tanstack/devtools-client@0.0.5':
dependencies:
'@tanstack/devtools-event-client': 0.4.0
'@tanstack/devtools-event-bus@0.4.0':
dependencies:
ws: 8.18.3
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@tanstack/devtools-event-client@0.3.5': {}
'@tanstack/devtools-event-client@0.4.0': {}
'@tanstack/devtools-ui@0.4.4(csstype@3.2.3)(solid-js@1.9.10)':
dependencies:
clsx: 2.1.1
goober: 2.1.18(csstype@3.2.3)
solid-js: 1.9.10
transitivePeerDependencies:
- csstype
'@tanstack/devtools-utils@0.0.9(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10)':
dependencies:
'@tanstack/devtools-ui': 0.4.4(csstype@3.2.3)(solid-js@1.9.10)
optionalDependencies:
'@types/react': 19.2.7
react: 19.2.3
solid-js: 1.9.10
transitivePeerDependencies:
- csstype
'@tanstack/devtools@0.10.1(csstype@3.2.3)(solid-js@1.9.10)':
dependencies:
'@solid-primitives/event-listener': 2.4.3(solid-js@1.9.10)
'@solid-primitives/keyboard': 1.3.3(solid-js@1.9.10)
'@solid-primitives/resize-observer': 2.1.3(solid-js@1.9.10)
'@tanstack/devtools-client': 0.0.5
'@tanstack/devtools-event-bus': 0.4.0
'@tanstack/devtools-ui': 0.4.4(csstype@3.2.3)(solid-js@1.9.10)
clsx: 2.1.1
goober: 2.1.18(csstype@3.2.3)
solid-js: 1.9.10
transitivePeerDependencies:
- bufferutil
- csstype
- utf-8-validate
'@tanstack/form-core@1.27.1':
dependencies:
'@tanstack/devtools-event-client': 0.3.5
'@tanstack/pacer': 0.15.4
'@tanstack/store': 0.7.7
'@tanstack/form-core@1.27.6':
dependencies:
'@tanstack/devtools-event-client': 0.4.0
'@tanstack/pacer-lite': 0.1.1
'@tanstack/store': 0.7.7
'@tanstack/form-devtools@0.2.9(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10)':
dependencies:
'@tanstack/devtools-ui': 0.4.4(csstype@3.2.3)(solid-js@1.9.10)
'@tanstack/devtools-utils': 0.0.9(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10)
'@tanstack/form-core': 1.27.6
clsx: 2.1.1
dayjs: 1.11.19
goober: 2.1.18(csstype@3.2.3)
solid-js: 1.9.10
transitivePeerDependencies:
- '@types/react'
- csstype
- react
- vue
'@tanstack/pacer-lite@0.1.1': {}
'@tanstack/pacer@0.15.4':
dependencies:
'@tanstack/devtools-event-client': 0.3.5
@ -11783,6 +12010,30 @@ snapshots:
'@tanstack/query-devtools@5.91.1': {}
'@tanstack/react-devtools@0.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10)':
dependencies:
'@tanstack/devtools': 0.10.1(csstype@3.2.3)(solid-js@1.9.10)
'@types/react': 19.2.7
'@types/react-dom': 19.2.3(@types/react@19.2.7)
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
transitivePeerDependencies:
- bufferutil
- csstype
- solid-js
- utf-8-validate
'@tanstack/react-form-devtools@0.2.9(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10)':
dependencies:
'@tanstack/devtools-utils': 0.0.9(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10)
'@tanstack/form-devtools': 0.2.9(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10)
react: 19.2.3
transitivePeerDependencies:
- '@types/react'
- csstype
- solid-js
- vue
'@tanstack/react-form@1.27.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@tanstack/form-core': 1.27.1
@ -12091,6 +12342,11 @@ snapshots:
dependencies:
undici-types: 6.21.0
'@types/node@20.19.27':
dependencies:
undici-types: 6.21.0
optional: true
'@types/papaparse@5.5.1':
dependencies:
'@types/node': 18.15.0
@ -14486,6 +14742,10 @@ snapshots:
globrex@0.1.2: {}
goober@2.1.18(csstype@3.2.3):
dependencies:
csstype: 3.2.3
got@11.8.6:
dependencies:
'@sindresorhus/is': 4.6.0
@ -14512,7 +14772,7 @@ snapshots:
happy-dom@20.0.11:
dependencies:
'@types/node': 20.19.26
'@types/node': 20.19.27
'@types/whatwg-mimetype': 3.0.2
whatwg-mimetype: 3.0.0
optional: true
@ -15127,7 +15387,7 @@ snapshots:
lexical@0.38.2: {}
lib0@0.2.115:
lib0@0.2.116:
dependencies:
isomorphic.js: 0.2.5
@ -17044,8 +17304,6 @@ snapshots:
dependencies:
xmlchars: 2.2.0
scheduler@0.26.0: {}
scheduler@0.27.0: {}
schema-utils@2.7.1:
@ -17089,6 +17347,12 @@ snapshots:
dependencies:
randombytes: 2.1.0
seroval-plugins@1.3.3(seroval@1.3.2):
dependencies:
seroval: 1.3.2
seroval@1.3.2: {}
setimmediate@1.0.5: {}
sha.js@2.4.12:
@ -17203,6 +17467,12 @@ snapshots:
smol-toml@1.5.2: {}
solid-js@1.9.10:
dependencies:
csstype: 3.2.3
seroval: 1.3.2
seroval-plugins: 1.3.3(seroval@1.3.2)
sortablejs@1.15.6: {}
source-list-map@2.0.1: {}
@ -17734,10 +18004,10 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.7
use-context-selector@2.0.0(react@19.2.3)(scheduler@0.26.0):
use-context-selector@2.0.0(react@19.2.3)(scheduler@0.27.0):
dependencies:
react: 19.2.3
scheduler: 0.26.0
scheduler: 0.27.0
use-isomorphic-layout-effect@1.2.1(@types/react@19.2.7)(react@19.2.3):
dependencies:
@ -18202,7 +18472,7 @@ snapshots:
yjs@13.6.27:
dependencies:
lib0: 0.2.115
lib0: 0.2.116
yocto-queue@0.1.0: {}