From 9a2b792f08117e125b1b33a17dfb5165f3005fe1 Mon Sep 17 00:00:00 2001 From: Untone Date: Sun, 17 Aug 2025 17:56:31 +0300 Subject: [PATCH] refactored --- .github/workflows/deploy.yml | 24 +- .gitignore | 2 +- CHANGELOG.md | 16 + README.md | 4 +- auth/__init__.py | 2 +- auth/core.py | 13 +- auth/decorators.py | 16 +- auth/identity.py | 4 +- auth/internal.py | 4 +- auth/jwtcodec.py | 4 +- auth/middleware.py | 4 +- auth/oauth.py | 4 +- auth/state.py | 1 - auth/tokens/batch.py | 2 +- auth/tokens/monitoring.py | 2 +- auth/tokens/oauth.py | 6 +- auth/tokens/sessions.py | 2 +- auth/tokens/verification.py | 2 +- auth/utils.py | 24 +- cache/cache.py | 4 +- cache/precache.py | 7 +- cache/revalidator.py | 2 +- cache/triggers.py | 5 +- scripts/ci-server.py => ci-server.py | 51 +- docs/auth-migration.md | 6 +- docs/testing.md | 2 +- main.py | 12 +- orm/community.py | 26 +- orm/draft.py | 1 + orm/notification.py | 1 + orm/reaction.py | 1 + orm/shout.py | 19 +- orm/topic.py | 1 + package-lock.json | 852 +++++++++--------- package.json | 10 +- pyproject.toml | 1 + services/rbac_init.py => rbac/__init__.py | 15 +- services/rbac.py => rbac/api.py | 23 +- .../default_role_permissions.json | 0 auth/rbac_interface.py => rbac/interface.py | 15 +- services/rbac_impl.py => rbac/operations.py | 17 +- {auth => rbac}/permissions.py | 0 {services => rbac}/permissions_catalog.json | 0 resolvers/admin.py | 10 +- resolvers/auth.py | 2 +- resolvers/author.py | 12 +- resolvers/bookmark.py | 6 +- resolvers/collab.py | 4 +- resolvers/collection.py | 6 +- resolvers/community.py | 6 +- resolvers/draft.py | 4 +- resolvers/editor.py | 6 +- resolvers/feed.py | 4 +- resolvers/follower.py | 6 +- resolvers/notifier.py | 4 +- resolvers/proposals.py | 2 +- resolvers/rating.py | 4 +- resolvers/reaction.py | 4 +- resolvers/reader.py | 4 +- resolvers/stat.py | 6 +- resolvers/topic.py | 8 +- scripts/test-ci-local.sh | 119 --- services/admin.py | 11 +- services/auth.py | 16 +- services/notify.py | 4 +- services/search.py | 2 +- services/viewed.py | 4 +- storage/__init__.py | 0 {services => storage}/db.py | 0 {services => storage}/env.py | 2 +- {services => storage}/redis.py | 0 {services => storage}/schema.py | 5 +- tests/auth/test_oauth.py | 2 +- tests/conftest.py | 6 +- tests/test_admin_panel_fixes.py | 2 +- tests/test_auth_fixes.py | 2 +- tests/test_community_creator_fix.py | 2 +- tests/test_community_rbac.py | 4 +- tests/test_config.py | 4 +- tests/test_coverage_imports.py | 32 +- tests/test_custom_roles.py | 4 +- tests/test_db_coverage.py | 2 +- tests/test_drafts.py | 8 +- tests/test_follow_fix.py | 2 +- tests/test_rbac_debug.py | 4 +- tests/test_rbac_integration.py | 6 +- tests/test_rbac_system.py | 10 +- tests/test_redis_coverage.py | 16 +- tests/test_redis_functionality.py | 2 +- tests/test_simple_unfollow_test.py | 2 +- tests/test_unfollow_fix.py | 4 +- tests/test_unpublish_shout.py | 2 +- tests/test_update_security.py | 2 +- {services => utils}/common_result.py | 6 - utils/encoders.py | 6 +- {services => utils}/exception.py | 0 utils/generate_slug.py | 2 +- {services => utils}/sentry.py | 6 +- 98 files changed, 702 insertions(+), 904 deletions(-) rename scripts/ci-server.py => ci-server.py (93%) rename services/rbac_init.py => rbac/__init__.py (57%) rename services/rbac.py => rbac/api.py (98%) rename {services => rbac}/default_role_permissions.json (100%) rename auth/rbac_interface.py => rbac/interface.py (88%) rename services/rbac_impl.py => rbac/operations.py (94%) rename {auth => rbac}/permissions.py (100%) rename {services => rbac}/permissions_catalog.json (100%) delete mode 100755 scripts/test-ci-local.sh create mode 100644 storage/__init__.py rename {services => storage}/db.py (100%) rename {services => storage}/env.py (99%) rename {services => storage}/redis.py (100%) rename {services => storage}/schema.py (98%) rename {services => utils}/common_result.py (85%) rename {services => utils}/exception.py (100%) rename {services => utils}/sentry.py (84%) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bf70954a..f51492b9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -137,7 +137,7 @@ jobs: from orm.shout import Shout from orm.topic import Topic from auth.orm import Author, AuthorBookmark, AuthorRating, AuthorFollower - from services.db import engine + from storage.db import engine from sqlalchemy import inspect print('✅ Engine imported successfully') @@ -166,8 +166,8 @@ jobs: - name: Start servers run: | - chmod +x scripts/ci-server.py - timeout 300 python scripts/ci-server.py & + chmod +x ./ci-server.py + timeout 300 python ./ci-server.py & echo $! > ci-server.pid echo "Waiting for servers..." @@ -184,8 +184,13 @@ jobs: # Создаем папку для результатов тестов mkdir -p test-results - # В CI пропускаем тесты здоровья серверов, так как они могут не пройти - echo "🏥 В CI режиме пропускаем тесты здоровья серверов..." + # Сначала проверяем здоровье серверов + echo "🏥 Проверяем здоровье серверов..." + if uv run pytest tests/test_server_health.py -v; then + echo "✅ Серверы здоровы!" + else + echo "⚠️ Тест здоровья серверов не прошел, но продолжаем..." + fi for test_type in "not e2e" "integration" "e2e" "browser"; do echo "Running $test_type tests..." @@ -284,19 +289,20 @@ jobs: fetch-depth: 0 - name: Deploy - if: github.ref == 'refs/heads/dev' env: HOST_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + TARGET: ${{ github.ref == 'refs/heads/dev' && 'core' || 'discoursio-api' }} + SERVER: ${{ github.ref == 'refs/heads/dev' && 'STAGING' || 'V' }} run: | - echo "🚀 Deploying to $SERVER..." + echo "🚀 Deploying to $ENV..." mkdir -p ~/.ssh echo "$HOST_KEY" > ~/.ssh/known_hosts chmod 600 ~/.ssh/known_hosts - git remote add dokku dokku@v3.dscrs.site:core + git remote add dokku dokku@staging.discours.io:$TARGET git push dokku HEAD:main -f - echo "✅ deployment completed!" + echo "✅ $ENV deployment completed!" # ===== SUMMARY ===== summary: diff --git a/.gitignore b/.gitignore index d592f123..fa004f22 100644 --- a/.gitignore +++ b/.gitignore @@ -177,5 +177,5 @@ panel/types.gen.ts tmp test-results page_content.html - +test_output docs/progress/* \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 14849db0..1358f628 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ Все значимые изменения в проекте документируются в этом файле. +## [0.9.7] - 2025-08-17 + +### 🔧 Исправления архитектуры +- **Устранены циклические импорты в ORM**: Исправлена проблема с циклическими импортами между `orm/community.py` и `orm/shout.py` +- **Оптимизированы импорты моделей**: Убран прямой импорт `Shout` из `orm/community.py`, заменен на строковые ссылки +- **Исправлены предупреждения ruff**: Добавлены `# noqa: PLW0603` комментарии для подавления предупреждений о `global` в `rbac/interface.py` +- **Улучшена совместимость SQLAlchemy**: Использование `text()` для сложных SQL выражений в `CommunityStats` + +### 🏷️ Типизация +- **Исправлены mypy ошибки**: Все ORM модели теперь корректно проходят проверку типов +- **Улучшена совместимость**: Использование `BaseModel` вместо алиаса `Base` для избежания путаницы + +### 🧹 Код-качество +- **Упрощена архитектура импортов**: Убраны сложные конструкции для избежания `global` +- **Сохранена функциональность**: Все методы `CommunityStats` работают корректно с новой архитектурой + ## [0.9.6] - 2025-08-12 ### 🚀 CI/CD и E2E тестирование diff --git a/README.md b/README.md index a2ed320d..73535fe4 100644 --- a/README.md +++ b/README.md @@ -134,11 +134,11 @@ chmod +x scripts/test-ci-local.sh ``` ### CI Server Management -The `scripts/ci-server.py` script manages servers for CI: +The `./ci-server.py` script manages servers for CI: ```bash # Start servers in CI mode -CI_MODE=true python3 scripts/ci-server.py +CI_MODE=true python3 ./ci-server.py ``` ## 📊 Project Structure diff --git a/auth/__init__.py b/auth/__init__.py index ce2b6967..8c2519b9 100644 --- a/auth/__init__.py +++ b/auth/__init__.py @@ -5,7 +5,7 @@ from starlette.responses import JSONResponse, RedirectResponse, Response from auth.core import verify_internal_auth from auth.orm import Author from auth.tokens.storage import TokenStorage -from services.db import local_session +from storage.db import local_session from settings import ( SESSION_COOKIE_HTTPONLY, SESSION_COOKIE_MAX_AGE, diff --git a/auth/core.py b/auth/core.py index b7802e32..1090c3f4 100644 --- a/auth/core.py +++ b/auth/core.py @@ -4,12 +4,14 @@ """ import time + from sqlalchemy.orm.exc import NoResultFound + +from auth.orm import Author from auth.state import AuthState from auth.tokens.storage import TokenStorage as TokenManager -from auth.orm import Author from orm.community import CommunityAuthor -from services.db import local_session +from storage.db import local_session from settings import ADMIN_EMAILS as ADMIN_EMAILS_LIST from utils.logger import root_logger as logger @@ -50,7 +52,7 @@ async def verify_internal_auth(token: str) -> tuple[int, list, bool]: with local_session() as session: try: # Author уже импортирован в начале файла - + author = session.query(Author).where(Author.id == user_id).one() # Получаем роли @@ -112,14 +114,14 @@ async def get_auth_token_from_request(request) -> str | None: """ # Отложенный импорт для избежания циклических зависимостей from auth.decorators import get_auth_token - + return await get_auth_token(request) async def authenticate(request) -> AuthState: """ Получает токен из запроса и проверяет авторизацию. - + Args: request: Объект запроса @@ -146,4 +148,3 @@ async def authenticate(request) -> AuthState: auth_state.author_id = str(user_id) auth_state.is_admin = is_admin return auth_state - diff --git a/auth/decorators.py b/auth/decorators.py index 8b397039..30a41585 100644 --- a/auth/decorators.py +++ b/auth/decorators.py @@ -5,28 +5,20 @@ from typing import Any from graphql import GraphQLError, GraphQLResolveInfo from sqlalchemy import exc -from auth.credentials import AuthCredentials -from auth.exceptions import OperationNotAllowedError # Импорт базовых функций из реструктурированных модулей from auth.core import authenticate -from auth.utils import get_auth_token +from auth.credentials import AuthCredentials +from auth.exceptions import OperationNotAllowedError from auth.orm import Author +from auth.utils import get_auth_token, get_safe_headers from orm.community import CommunityAuthor -from services.db import local_session -from services.redis import redis as redis_adapter +from storage.db import local_session from settings import ADMIN_EMAILS as ADMIN_EMAILS_LIST from utils.logger import root_logger as logger ADMIN_EMAILS = ADMIN_EMAILS_LIST.split(",") -# Импортируем get_safe_headers из utils -from auth.utils import get_safe_headers - - -# get_auth_token теперь импортирован из auth.utils - - async def validate_graphql_context(info: GraphQLResolveInfo) -> None: """ Проверяет валидность GraphQL контекста и проверяет авторизацию. diff --git a/auth/identity.py b/auth/identity.py index 146c9663..46efcebf 100644 --- a/auth/identity.py +++ b/auth/identity.py @@ -4,8 +4,8 @@ from auth.exceptions import ExpiredTokenError, InvalidPasswordError, InvalidToke from auth.jwtcodec import JWTCodec from auth.orm import Author from auth.password import Password -from services.db import local_session -from services.redis import redis +from storage.db import local_session +from storage.redis import redis from utils.logger import root_logger as logger AuthorType = TypeVar("AuthorType", bound=Author) diff --git a/auth/internal.py b/auth/internal.py index 588c55d1..439c25a7 100644 --- a/auth/internal.py +++ b/auth/internal.py @@ -7,7 +7,7 @@ DEPRECATED: Этот модуль переносится в auth/core.py """ # Импорт базовых функций из core модуля -from auth.core import verify_internal_auth, create_internal_session, authenticate +from auth.core import authenticate, create_internal_session, verify_internal_auth # Re-export для обратной совместимости -__all__ = ["verify_internal_auth", "create_internal_session", "authenticate"] \ No newline at end of file +__all__ = ["authenticate", "create_internal_session", "verify_internal_auth"] diff --git a/auth/jwtcodec.py b/auth/jwtcodec.py index 8a98770f..795a7495 100644 --- a/auth/jwtcodec.py +++ b/auth/jwtcodec.py @@ -40,9 +40,7 @@ class JWTCodec: # Если время истечения не указано, устанавливаем дефолтное if not expiration: - expiration = datetime.datetime.now(datetime.UTC) + datetime.timedelta( - days=JWT_REFRESH_TOKEN_EXPIRE_DAYS - ) + expiration = datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=JWT_REFRESH_TOKEN_EXPIRE_DAYS) logger.debug(f"[JWTCodec.encode] Время истечения не указано, устанавливаем срок: {expiration}") # Формируем payload с временными метками diff --git a/auth/middleware.py b/auth/middleware.py index 4e485cf9..9d65f205 100644 --- a/auth/middleware.py +++ b/auth/middleware.py @@ -17,8 +17,8 @@ from starlette.types import ASGIApp from auth.credentials import AuthCredentials from auth.orm import Author from auth.tokens.storage import TokenStorage as TokenManager -from services.db import local_session -from services.redis import redis as redis_adapter +from storage.db import local_session +from storage.redis import redis as redis_adapter from settings import ( ADMIN_EMAILS as ADMIN_EMAILS_LIST, ) diff --git a/auth/oauth.py b/auth/oauth.py index 239a6874..37feda17 100644 --- a/auth/oauth.py +++ b/auth/oauth.py @@ -13,8 +13,8 @@ from starlette.responses import JSONResponse, RedirectResponse from auth.orm import Author from auth.tokens.storage import TokenStorage from orm.community import Community, CommunityAuthor, CommunityFollower -from services.db import local_session -from services.redis import redis +from storage.db import local_session +from storage.redis import redis from settings import ( FRONTEND_URL, OAUTH_CLIENTS, diff --git a/auth/state.py b/auth/state.py index 9bc0aa69..d54e8648 100644 --- a/auth/state.py +++ b/auth/state.py @@ -3,7 +3,6 @@ """ - class AuthState: """ Класс для хранения информации о состоянии авторизации пользователя. diff --git a/auth/tokens/batch.py b/auth/tokens/batch.py index 73bd0e38..d265a720 100644 --- a/auth/tokens/batch.py +++ b/auth/tokens/batch.py @@ -6,7 +6,7 @@ import asyncio from typing import Any, Dict, List from auth.jwtcodec import JWTCodec -from services.redis import redis as redis_adapter +from storage.redis import redis as redis_adapter from utils.logger import root_logger as logger from .base import BaseTokenManager diff --git a/auth/tokens/monitoring.py b/auth/tokens/monitoring.py index 788884e3..342480cf 100644 --- a/auth/tokens/monitoring.py +++ b/auth/tokens/monitoring.py @@ -5,7 +5,7 @@ import asyncio from typing import Any, Dict -from services.redis import redis as redis_adapter +from storage.redis import redis as redis_adapter from utils.logger import root_logger as logger from .base import BaseTokenManager diff --git a/auth/tokens/oauth.py b/auth/tokens/oauth.py index 5f4c9fa3..6978cdd9 100644 --- a/auth/tokens/oauth.py +++ b/auth/tokens/oauth.py @@ -5,7 +5,7 @@ import json import time -from services.redis import redis as redis_adapter +from storage.redis import redis as redis_adapter from utils.logger import root_logger as logger from .base import BaseTokenManager @@ -84,9 +84,7 @@ class OAuthTokenManager(BaseTokenManager): return await self._get_oauth_data_optimized(token_type, str(user_id), provider) return None - async def _get_oauth_data_optimized( - self, token_type: TokenType, user_id: str, provider: str - ) -> TokenData | None: + async def _get_oauth_data_optimized(self, token_type: TokenType, user_id: str, provider: str) -> TokenData | None: """Оптимизированное получение OAuth данных""" if not user_id or not provider: error_msg = "OAuth токены требуют user_id и provider" diff --git a/auth/tokens/sessions.py b/auth/tokens/sessions.py index 71130932..00e3d637 100644 --- a/auth/tokens/sessions.py +++ b/auth/tokens/sessions.py @@ -7,7 +7,7 @@ import time from typing import Any, List from auth.jwtcodec import JWTCodec -from services.redis import redis as redis_adapter +from storage.redis import redis as redis_adapter from utils.logger import root_logger as logger from .base import BaseTokenManager diff --git a/auth/tokens/verification.py b/auth/tokens/verification.py index 2b28fc7b..9d2ec5b0 100644 --- a/auth/tokens/verification.py +++ b/auth/tokens/verification.py @@ -6,7 +6,7 @@ import json import secrets import time -from services.redis import redis as redis_adapter +from storage.redis import redis as redis_adapter from utils.logger import root_logger as logger from .base import BaseTokenManager diff --git a/auth/utils.py b/auth/utils.py index 5ceb7ac1..9ca361b0 100644 --- a/auth/utils.py +++ b/auth/utils.py @@ -4,6 +4,7 @@ """ from typing import Any + from settings import SESSION_COOKIE_NAME, SESSION_TOKEN_HEADER from utils.logger import root_logger as logger @@ -113,26 +114,25 @@ async def get_auth_token(request: Any) -> str | None: token = auth_header.replace("Bearer ", "", 1).strip() logger.debug(f"[decorators] Извлечен Bearer токен: {len(token)}") return token - else: - logger.debug("[decorators] Authorization заголовок не содержит Bearer токен") + logger.debug("[decorators] Authorization заголовок не содержит Bearer токен") # 6. Проверяем cookies if hasattr(request, "cookies") and request.cookies: if isinstance(request.cookies, dict): cookies = request.cookies elif hasattr(request.cookies, "get"): - cookies = {k: request.cookies.get(k) for k in getattr(request.cookies, "keys", lambda: [])()} + cookies = {k: request.cookies.get(k) for k in getattr(request.cookies, "keys", list)()} else: cookies = {} - + logger.debug(f"[decorators] Доступные cookies: {list(cookies.keys())}") - + # Проверяем кастомную cookie if SESSION_COOKIE_NAME in cookies: token = cookies[SESSION_COOKIE_NAME] logger.debug(f"[decorators] Токен найден в cookie {SESSION_COOKIE_NAME}: {len(token)}") return token - + # Проверяем стандартную cookie if "auth_token" in cookies: token = cookies["auth_token"] @@ -150,29 +150,29 @@ async def get_auth_token(request: Any) -> str | None: def extract_bearer_token(auth_header: str) -> str | None: """ Извлекает токен из заголовка Authorization с Bearer схемой. - + Args: auth_header: Заголовок Authorization - + Returns: Optional[str]: Извлеченный токен или None """ if not auth_header: return None - + if auth_header.startswith("Bearer "): return auth_header[7:].strip() - + return None def format_auth_header(token: str) -> str: """ Форматирует токен в заголовок Authorization. - + Args: token: Токен авторизации - + Returns: str: Отформатированный заголовок """ diff --git a/cache/cache.py b/cache/cache.py index 81995025..51228acd 100644 --- a/cache/cache.py +++ b/cache/cache.py @@ -37,8 +37,8 @@ from sqlalchemy import and_, join, select from auth.orm import Author, AuthorFollower from orm.shout import Shout, ShoutAuthor, ShoutTopic from orm.topic import Topic, TopicFollower -from services.db import local_session -from services.redis import redis +from storage.db import local_session +from storage.redis import redis from utils.encoders import fast_json_dumps from utils.logger import root_logger as logger diff --git a/cache/precache.py b/cache/precache.py index 97f6f703..a4c9e853 100644 --- a/cache/precache.py +++ b/cache/precache.py @@ -3,14 +3,15 @@ import traceback from sqlalchemy import and_, join, select +from auth.orm import Author, AuthorFollower + # Импорт Author, AuthorFollower отложен для избежания циклических импортов from cache.cache import cache_author, cache_topic from orm.shout import Shout, ShoutAuthor, ShoutReactionsFollower, ShoutTopic from orm.topic import Topic, TopicFollower from resolvers.stat import get_with_stat -from auth.orm import Author, AuthorFollower -from services.db import local_session -from services.redis import redis +from storage.db import local_session +from storage.redis import redis from utils.encoders import fast_json_dumps from utils.logger import root_logger as logger diff --git a/cache/revalidator.py b/cache/revalidator.py index 76ebdf3a..038823ca 100644 --- a/cache/revalidator.py +++ b/cache/revalidator.py @@ -9,7 +9,7 @@ from cache.cache import ( invalidate_cache_by_prefix, ) from resolvers.stat import get_with_stat -from services.redis import redis +from storage.redis import redis from utils.logger import root_logger as logger CACHE_REVALIDATION_INTERVAL = 300 # 5 minutes diff --git a/cache/triggers.py b/cache/triggers.py index 4a28726e..1536dfb9 100644 --- a/cache/triggers.py +++ b/cache/triggers.py @@ -1,12 +1,13 @@ from sqlalchemy import event +from auth.orm import Author, AuthorFollower + # Импорт Author, AuthorFollower отложен для избежания циклических импортов from cache.revalidator import revalidation_manager from orm.reaction import Reaction, ReactionKind from orm.shout import Shout, ShoutAuthor, ShoutReactionsFollower from orm.topic import Topic, TopicFollower -from services.db import local_session -from auth.orm import Author, AuthorFollower +from storage.db import local_session from utils.logger import root_logger as logger diff --git a/scripts/ci-server.py b/ci-server.py similarity index 93% rename from scripts/ci-server.py rename to ci-server.py index 734c24f6..99f4663e 100755 --- a/scripts/ci-server.py +++ b/ci-server.py @@ -20,34 +20,8 @@ import requests from sqlalchemy import inspect from orm.base import Base -from services.db import engine - - -# Создаем собственный логгер без дублирования -def create_ci_logger(): - """Создает логгер для CI без дублирования""" - logger = logging.getLogger("ci-server") - logger.setLevel(logging.INFO) - - # Убираем существующие обработчики - logger.handlers.clear() - - # Создаем форматтер - formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") - - # Создаем обработчик - handler = logging.StreamHandler() - handler.setFormatter(formatter) - logger.addHandler(handler) - - # Отключаем пропагацию к root logger - logger.propagate = False - - return logger - - -logger = create_ci_logger() - +from storage.db import engine +from utils.logger import root_logger as logger class CIServerManager: """Менеджер CI серверов""" @@ -257,9 +231,10 @@ def run_tests_in_ci(): try: ruff_result = subprocess.run( ["uv", "run", "ruff", "check", "."], - check=False, capture_output=False, + check=False, + capture_output=False, text=True, - timeout=300 # 5 минут на linting + timeout=300, # 5 минут на linting ) if ruff_result.returncode == 0: logger.info("✅ Ruff проверка прошла успешно") @@ -275,9 +250,10 @@ def run_tests_in_ci(): try: ruff_format_result = subprocess.run( ["uv", "run", "ruff", "format", "--check", "."], - check=False, capture_output=False, + check=False, + capture_output=False, text=True, - timeout=300 # 5 минут на проверку форматирования + timeout=300, # 5 минут на проверку форматирования ) if ruff_format_result.returncode == 0: logger.info("✅ Форматирование корректно") @@ -293,9 +269,10 @@ def run_tests_in_ci(): try: mypy_result = subprocess.run( ["uv", "run", "mypy", ".", "--ignore-missing-imports"], - check=False, capture_output=False, + check=False, + capture_output=False, text=True, - timeout=600 # 10 минут на type checking + timeout=600, # 10 минут на type checking ) if mypy_result.returncode == 0: logger.info("✅ MyPy проверка прошла успешно") @@ -311,7 +288,8 @@ def run_tests_in_ci(): try: health_result = subprocess.run( ["uv", "run", "pytest", "tests/test_server_health.py", "-v"], - check=False, capture_output=False, + check=False, + capture_output=False, text=True, timeout=120, # 2 минуты на проверку здоровья ) @@ -339,7 +317,8 @@ def run_tests_in_ci(): # Запускаем тесты с выводом в реальном времени result = subprocess.run( cmd, - check=False, capture_output=False, # Потоковый вывод + check=False, + capture_output=False, # Потоковый вывод text=True, timeout=600, # 10 минут на тесты ) diff --git a/docs/auth-migration.md b/docs/auth-migration.md index 80c66f8b..9ef29a11 100644 --- a/docs/auth-migration.md +++ b/docs/auth-migration.md @@ -61,7 +61,7 @@ await TokenStorage.revoke_session(token) #### Обновленный API: ```python -from services.redis import redis +from storage.redis import redis # Базовые операции await redis.get(key) @@ -190,7 +190,7 @@ compat = CompatibilityMethods() await compat.get(token_key) # Стало -from services.redis import redis +from storage.redis import redis result = await redis.get(token_key) ``` @@ -263,7 +263,7 @@ pytest tests/auth/ -v # Проверка Redis подключения python -c " import asyncio -from services.redis import redis +from storage.redis import redis async def test(): result = await redis.ping() print(f'Redis connection: {result}') diff --git a/docs/testing.md b/docs/testing.md index 9f132950..68c9bb29 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -210,7 +210,7 @@ class MockInfo: self.field_nodes = [MockFieldNode(requested_fields or [])] # Патчинг зависимостей -@patch('services.redis.aioredis') +@patch('storage.redis.aioredis') def test_redis_connection(mock_aioredis): # Тест логики pass diff --git a/main.py b/main.py index 17c3a94b..5febbeb2 100644 --- a/main.py +++ b/main.py @@ -21,10 +21,10 @@ from auth.middleware import AuthMiddleware, auth_middleware from auth.oauth import oauth_callback, oauth_login from cache.precache import precache_data from cache.revalidator import revalidation_manager -from services.exception import ExceptionHandlerMiddleware -from services.rbac_init import initialize_rbac -from services.redis import redis -from services.schema import create_all_tables, resolvers +from rbac import initialize_rbac +from utils.exception import ExceptionHandlerMiddleware +from storage.redis import redis +from storage.schema import create_all_tables, resolvers from services.search import check_search_service, initialize_search_index_background, search_service from services.viewed import ViewedStorage from settings import DEV_SERVER_PID_FILE_NAME @@ -211,10 +211,10 @@ async def lifespan(app: Starlette): try: print("[lifespan] Starting application initialization") create_all_tables() - + # Инициализируем RBAC систему с dependency injection initialize_rbac() - + await asyncio.gather( redis.connect(), precache_data(), diff --git a/orm/community.py b/orm/community.py index 414b18c1..19573d0f 100644 --- a/orm/community.py +++ b/orm/community.py @@ -13,15 +13,15 @@ from sqlalchemy import ( UniqueConstraint, distinct, func, + text, ) from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import Mapped, mapped_column from auth.orm import Author from orm.base import BaseModel -from orm.shout import Shout -from services.db import local_session -from auth.rbac_interface import get_rbac_operations +from rbac.interface import get_rbac_operations +from storage.db import local_session # Словарь названий ролей role_names = { @@ -355,7 +355,13 @@ class CommunityStats: @property def shouts(self) -> int: - return self.community.session.query(func.count(Shout.id)).filter(Shout.community == self.community.id).scalar() + return ( + self.community.session.query(func.count(1)) + .select_from(text("shout")) + .filter(text("shout.community_id = :community_id")) + .params(community_id=self.community.id) + .scalar() + ) @property def followers(self) -> int: @@ -370,12 +376,10 @@ class CommunityStats: # author has a shout with community id and its featured_at is not null return ( self.community.session.query(func.count(distinct(Author.id))) - .join(Shout) - .filter( - Shout.community == self.community.id, - Shout.featured_at.is_not(None), - Author.id.in_(Shout.authors), - ) + .select_from(text("author")) + .join(text("shout"), text("author.id IN (SELECT author_id FROM shout_author WHERE shout_id = shout.id)")) + .filter(text("shout.community_id = :community_id"), text("shout.featured_at IS NOT NULL")) + .params(community_id=self.community.id) .scalar() ) @@ -498,7 +502,7 @@ class CommunityAuthor(BaseModel): # Используем fallback на проверку ролей return permission in self.role_list except Exception: - # FIXME: Fallback: проверяем роли (старый способ) + # TODO: Fallback: проверяем роли (старый способ) return any(permission == role for role in self.role_list) def dict(self, access: bool = False) -> dict[str, Any]: diff --git a/orm/draft.py b/orm/draft.py index 1948af9f..df63ca35 100644 --- a/orm/draft.py +++ b/orm/draft.py @@ -8,6 +8,7 @@ from auth.orm import Author from orm.base import BaseModel as Base from orm.topic import Topic + # Author уже импортирован в начале файла def get_author_model(): """Возвращает модель Author для использования в запросах""" diff --git a/orm/notification.py b/orm/notification.py index 0adfb558..30a6bd40 100644 --- a/orm/notification.py +++ b/orm/notification.py @@ -10,6 +10,7 @@ from auth.orm import Author from orm.base import BaseModel as Base from utils.logger import root_logger as logger + # Author уже импортирован в начале файла def get_author_model(): """Возвращает модель Author для использования в запросах""" diff --git a/orm/reaction.py b/orm/reaction.py index 87902df1..c4edf6fa 100644 --- a/orm/reaction.py +++ b/orm/reaction.py @@ -7,6 +7,7 @@ from sqlalchemy.orm import Mapped, mapped_column from auth.orm import Author from orm.base import BaseModel as Base + # Author уже импортирован в начале файла def get_author_model(): """Возвращает модель Author для использования в запросах""" diff --git a/orm/shout.py b/orm/shout.py index 95f62b33..d9992db3 100644 --- a/orm/shout.py +++ b/orm/shout.py @@ -4,19 +4,10 @@ from typing import Any from sqlalchemy import JSON, Boolean, ForeignKey, Index, Integer, PrimaryKeyConstraint, String from sqlalchemy.orm import Mapped, mapped_column, relationship -# Импорт Author отложен для избежания циклических импортов -from auth.orm import Author -from orm.base import BaseModel as Base -from orm.reaction import Reaction -from orm.topic import Topic - -# Author уже импортирован в начале файла -def get_author_model(): - """Возвращает модель Author для использования в запросах""" - return Author +from orm.base import BaseModel -class ShoutTopic(Base): +class ShoutTopic(BaseModel): """ Связь между публикацией и темой. @@ -40,7 +31,7 @@ class ShoutTopic(Base): ) -class ShoutReactionsFollower(Base): +class ShoutReactionsFollower(BaseModel): __tablename__ = "shout_reactions_followers" follower: Mapped[int] = mapped_column(ForeignKey("author.id"), index=True) @@ -57,7 +48,7 @@ class ShoutReactionsFollower(Base): ) -class ShoutAuthor(Base): +class ShoutAuthor(BaseModel): """ Связь между публикацией и автором. @@ -81,7 +72,7 @@ class ShoutAuthor(Base): ) -class Shout(Base): +class Shout(BaseModel): """ Публикация в системе. """ diff --git a/orm/topic.py b/orm/topic.py index fd6f08de..c15451c7 100644 --- a/orm/topic.py +++ b/orm/topic.py @@ -14,6 +14,7 @@ from sqlalchemy.orm import Mapped, mapped_column from auth.orm import Author from orm.base import BaseModel as Base + # Author уже импортирован в начале файла def get_author_model(): """Возвращает модель Author для использования в запросах""" diff --git a/package-lock.json b/package-lock.json index 53325db9..3d962b29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,9 +7,6 @@ "": { "name": "publy-panel", "version": "0.9.5", - "dependencies": { - "@solidjs/router": "^0.15.3" - }, "devDependencies": { "@biomejs/biome": "^2.1.2", "@graphql-codegen/cli": "^5.0.7", @@ -17,18 +14,17 @@ "@graphql-codegen/typescript": "^4.1.6", "@graphql-codegen/typescript-operations": "^4.6.1", "@graphql-codegen/typescript-resolvers": "^4.5.1", + "@solidjs/router": "^0.15.3", "@types/node": "^24.1.0", - "@types/prettier": "^2.7.3", "@types/prismjs": "^1.26.5", "graphql": "^16.11.0", "graphql-tag": "^2.12.6", "lightningcss": "^1.30.1", - "prettier": "^3.6.2", "prismjs": "^1.30.0", "solid-js": "^1.9.7", "terser": "^5.43.0", "typescript": "^5.8.3", - "vite": "^7.0.6", + "vite": "^7.1.2", "vite-plugin-solid": "^2.11.7" } }, @@ -97,22 +93,22 @@ } }, "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -128,14 +124,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -186,15 +182,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -244,9 +240,9 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", - "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", "dev": true, "license": "MIT", "dependencies": { @@ -258,13 +254,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.28.2" }, "bin": { "parser": "bin/babel-parser.js" @@ -306,9 +302,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", - "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", + "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", "dev": true, "license": "MIT", "engines": { @@ -331,18 +327,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", + "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", + "@babel/types": "^7.28.2", "debug": "^4.3.1" }, "engines": { @@ -364,9 +360,9 @@ } }, "node_modules/@biomejs/biome": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.1.2.tgz", - "integrity": "sha512-yq8ZZuKuBVDgAS76LWCfFKHSYIAgqkxVB3mGVVpOe2vSkUTs7xG46zXZeNPRNVjiJuw0SZ3+J2rXiYx0RUpfGg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.2.0.tgz", + "integrity": "sha512-3On3RSYLsX+n9KnoSgfoYlckYBoU6VRM22cw1gB4Y0OuUVSYd/O/2saOJMrA4HFfA1Ff0eacOvMN1yAAvHtzIw==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -380,20 +376,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.1.2", - "@biomejs/cli-darwin-x64": "2.1.2", - "@biomejs/cli-linux-arm64": "2.1.2", - "@biomejs/cli-linux-arm64-musl": "2.1.2", - "@biomejs/cli-linux-x64": "2.1.2", - "@biomejs/cli-linux-x64-musl": "2.1.2", - "@biomejs/cli-win32-arm64": "2.1.2", - "@biomejs/cli-win32-x64": "2.1.2" + "@biomejs/cli-darwin-arm64": "2.2.0", + "@biomejs/cli-darwin-x64": "2.2.0", + "@biomejs/cli-linux-arm64": "2.2.0", + "@biomejs/cli-linux-arm64-musl": "2.2.0", + "@biomejs/cli-linux-x64": "2.2.0", + "@biomejs/cli-linux-x64-musl": "2.2.0", + "@biomejs/cli-win32-arm64": "2.2.0", + "@biomejs/cli-win32-x64": "2.2.0" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.1.2.tgz", - "integrity": "sha512-leFAks64PEIjc7MY/cLjE8u5OcfBKkcDB0szxsWUB4aDfemBep1WVKt0qrEyqZBOW8LPHzrFMyDl3FhuuA0E7g==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.2.0.tgz", + "integrity": "sha512-zKbwUUh+9uFmWfS8IFxmVD6XwqFcENjZvEyfOxHs1epjdH3wyyMQG80FGDsmauPwS2r5kXdEM0v/+dTIA9FXAg==", "cpu": [ "arm64" ], @@ -408,9 +404,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.1.2.tgz", - "integrity": "sha512-Nmmv7wRX5Nj7lGmz0FjnWdflJg4zii8Ivruas6PBKzw5SJX/q+Zh2RfnO+bBnuKLXpj8kiI2x2X12otpH6a32A==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.2.0.tgz", + "integrity": "sha512-+OmT4dsX2eTfhD5crUOPw3RPhaR+SKVspvGVmSdZ9y9O/AgL8pla6T4hOn1q+VAFBHuHhsdxDRJgFCSC7RaMOw==", "cpu": [ "x64" ], @@ -425,9 +421,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.1.2.tgz", - "integrity": "sha512-NWNy2Diocav61HZiv2enTQykbPP/KrA/baS7JsLSojC7Xxh2nl9IczuvE5UID7+ksRy2e7yH7klm/WkA72G1dw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.2.0.tgz", + "integrity": "sha512-6eoRdF2yW5FnW9Lpeivh7Mayhq0KDdaDMYOJnH9aT02KuSIX5V1HmWJCQQPwIQbhDh68Zrcpl8inRlTEan0SXw==", "cpu": [ "arm64" ], @@ -442,9 +438,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.1.2.tgz", - "integrity": "sha512-qgHvafhjH7Oca114FdOScmIKf1DlXT1LqbOrrbR30kQDLFPEOpBG0uzx6MhmsrmhGiCFCr2obDamu+czk+X0HQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.0.tgz", + "integrity": "sha512-egKpOa+4FL9YO+SMUMLUvf543cprjevNc3CAgDNFLcjknuNMcZ0GLJYa3EGTCR2xIkIUJDVneBV3O9OcIlCEZQ==", "cpu": [ "arm64" ], @@ -459,9 +455,9 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.1.2.tgz", - "integrity": "sha512-Km/UYeVowygTjpX6sGBzlizjakLoMQkxWbruVZSNE6osuSI63i4uCeIL+6q2AJlD3dxoiBJX70dn1enjQnQqwA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.2.0.tgz", + "integrity": "sha512-5UmQx/OZAfJfi25zAnAGHUMuOd+LOsliIt119x2soA2gLggQYrVPA+2kMUxR6Mw5M1deUF/AWWP2qpxgH7Nyfw==", "cpu": [ "x64" ], @@ -476,9 +472,9 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.1.2.tgz", - "integrity": "sha512-xlB3mU14ZUa3wzLtXfmk2IMOGL+S0aHFhSix/nssWS/2XlD27q+S6f0dlQ8WOCbYoXcuz8BCM7rCn2lxdTrlQA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.0.tgz", + "integrity": "sha512-I5J85yWwUWpgJyC1CcytNSGusu2p9HjDnOPAFG4Y515hwRD0jpR9sT9/T1cKHtuCvEQ/sBvx+6zhz9l9wEJGAg==", "cpu": [ "x64" ], @@ -493,9 +489,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.1.2.tgz", - "integrity": "sha512-G8KWZli5ASOXA3yUQgx+M4pZRv3ND16h77UsdunUL17uYpcL/UC7RkWTdkfvMQvogVsAuz5JUcBDjgZHXxlKoA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.2.0.tgz", + "integrity": "sha512-n9a1/f2CwIDmNMNkFs+JI0ZjFnMO0jdOyGNtihgUNFnlmd84yIYY2KMTBmMV58ZlVHjgmY5Y6E1hVTnSRieggA==", "cpu": [ "arm64" ], @@ -510,9 +506,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.1.2.tgz", - "integrity": "sha512-9zajnk59PMpjBkty3bK2IrjUsUHvqe9HWwyAWQBjGLE7MIBjbX2vwv1XPEhmO2RRuGoTkVx3WCanHrjAytICLA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.0.tgz", + "integrity": "sha512-Nawu5nHjP/zPKTIryh2AavzTc/KEg4um/MxWdXW0A6P/RZOyIpa7+QSjeXwAwX/utJGaCoXRPWtF3m5U/bB3Ww==", "cpu": [ "x64" ], @@ -571,9 +567,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", - "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", "cpu": [ "ppc64" ], @@ -588,9 +584,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", - "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", "cpu": [ "arm" ], @@ -605,9 +601,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", - "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", "cpu": [ "arm64" ], @@ -622,9 +618,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", - "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", "cpu": [ "x64" ], @@ -639,9 +635,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", - "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", "cpu": [ "arm64" ], @@ -656,9 +652,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", - "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", "cpu": [ "x64" ], @@ -673,9 +669,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", - "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", "cpu": [ "arm64" ], @@ -690,9 +686,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", - "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", "cpu": [ "x64" ], @@ -707,9 +703,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", - "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", "cpu": [ "arm" ], @@ -724,9 +720,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", - "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", "cpu": [ "arm64" ], @@ -741,9 +737,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", - "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", "cpu": [ "ia32" ], @@ -758,9 +754,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", - "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", "cpu": [ "loong64" ], @@ -775,9 +771,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", - "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", "cpu": [ "mips64el" ], @@ -792,9 +788,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", - "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", "cpu": [ "ppc64" ], @@ -809,9 +805,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", - "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", "cpu": [ "riscv64" ], @@ -826,9 +822,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", - "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", "cpu": [ "s390x" ], @@ -843,9 +839,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", - "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", "cpu": [ "x64" ], @@ -860,9 +856,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", - "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", "cpu": [ "arm64" ], @@ -877,9 +873,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", - "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", "cpu": [ "x64" ], @@ -894,9 +890,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", - "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", "cpu": [ "arm64" ], @@ -911,9 +907,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", - "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", "cpu": [ "x64" ], @@ -928,9 +924,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", - "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", "cpu": [ "arm64" ], @@ -945,9 +941,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", - "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", "cpu": [ "x64" ], @@ -962,9 +958,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", - "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", "cpu": [ "arm64" ], @@ -979,9 +975,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", - "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", "cpu": [ "ia32" ], @@ -996,9 +992,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", - "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", "cpu": [ "x64" ], @@ -1013,9 +1009,9 @@ } }, "node_modules/@fastify/busboy": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.1.1.tgz", - "integrity": "sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz", + "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", "dev": true, "license": "MIT" }, @@ -1426,13 +1422,13 @@ } }, "node_modules/@graphql-tools/batch-execute": { - "version": "9.0.18", - "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-9.0.18.tgz", - "integrity": "sha512-KtBglqPGR/3CZtQevFRBBc6MJpIgxBqfCrUV5sdC3oJsafmPShgr+lxM178SW5i1QHmiVAScOWGWqWp9HbnpoQ==", + "version": "9.0.19", + "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-9.0.19.tgz", + "integrity": "sha512-VGamgY4PLzSx48IHPoblRw0oTaBa7S26RpZXt0Y4NN90ytoE0LutlpB2484RbkfcTjv9wa64QD474+YP1kEgGA==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^10.9.0", + "@graphql-tools/utils": "^10.9.1", "@whatwg-node/promise-helpers": "^1.3.0", "dataloader": "^2.2.3", "tslib": "^2.8.1" @@ -1465,16 +1461,16 @@ } }, "node_modules/@graphql-tools/delegate": { - "version": "10.2.22", - "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-10.2.22.tgz", - "integrity": "sha512-1jkTF5DIhO1YJ0dlgY03DZYAiSwlu5D2mdjeq+f6oyflyKG9E4SPmkLgVdDSNSfGxFHHrjIvYjUhPYV0vAOiDg==", + "version": "10.2.23", + "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-10.2.23.tgz", + "integrity": "sha512-xrPtl7f1LxS+B6o+W7ueuQh67CwRkfl+UKJncaslnqYdkxKmNBB4wnzVcW8ZsRdwbsla/v43PtwAvSlzxCzq2w==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/batch-execute": "^9.0.18", - "@graphql-tools/executor": "^1.4.8", - "@graphql-tools/schema": "^10.0.24", - "@graphql-tools/utils": "^10.9.0", + "@graphql-tools/batch-execute": "^9.0.19", + "@graphql-tools/executor": "^1.4.9", + "@graphql-tools/schema": "^10.0.25", + "@graphql-tools/utils": "^10.9.1", "@repeaterjs/repeater": "^3.0.6", "@whatwg-node/promise-helpers": "^1.3.0", "dataloader": "^2.2.3", @@ -1544,14 +1540,14 @@ } }, "node_modules/@graphql-tools/executor-graphql-ws": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-2.0.6.tgz", - "integrity": "sha512-hLmY+h1HDM4+y4EXP0SgNFd6hXEs4LCMAxvvdfPAwrzHNM04B0wnlcOi8Rze3e7AA9edxXQsm3UN4BE04U2OMg==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-2.0.7.tgz", + "integrity": "sha512-J27za7sKF6RjhmvSOwOQFeNhNHyP4f4niqPnerJmq73OtLx9Y2PGOhkXOEB0PjhvPJceuttkD2O1yMgEkTGs3Q==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/executor-common": "^0.0.5", - "@graphql-tools/utils": "^10.9.0", + "@graphql-tools/executor-common": "^0.0.6", + "@graphql-tools/utils": "^10.9.1", "@whatwg-node/disposablestack": "^0.0.6", "graphql-ws": "^6.0.6", "isomorphic-ws": "^5.0.0", @@ -1566,14 +1562,14 @@ } }, "node_modules/@graphql-tools/executor-graphql-ws/node_modules/@graphql-tools/executor-common": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-common/-/executor-common-0.0.5.tgz", - "integrity": "sha512-DBTQDGYajhUd4iBZ/yYc1LY85QTVhgTpGPCFT5iz0CPObgye0smsE5nd/BIcdbML7SXv2wFvQhVA3mCJJ32WuQ==", + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-common/-/executor-common-0.0.6.tgz", + "integrity": "sha512-JAH/R1zf77CSkpYATIJw+eOJwsbWocdDjY+avY7G+P5HCXxwQjAjWVkJI1QJBQYjPQDVxwf1fmTZlIN3VOadow==", "dev": true, "license": "MIT", "dependencies": { "@envelop/core": "^5.3.0", - "@graphql-tools/utils": "^10.9.0" + "@graphql-tools/utils": "^10.9.1" }, "engines": { "node": ">=18.0.0" @@ -1916,15 +1912,15 @@ } }, "node_modules/@graphql-tools/wrap": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-10.1.3.tgz", - "integrity": "sha512-YIcw7oZPlmlZKRBOQGNqKNY4lehB+U4NOP0BSuOd+23EZb8X7JjkruYUOjYsQ7GxS7aKmQpFbuqrfsLp9TRZnA==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-10.1.4.tgz", + "integrity": "sha512-7pyNKqXProRjlSdqOtrbnFRMQAVamCmEREilOXtZujxY6kYit3tvWWSjUrcIOheltTffoRh7EQSjpy2JDCzasg==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/delegate": "^10.2.22", - "@graphql-tools/schema": "^10.0.24", - "@graphql-tools/utils": "^10.9.0", + "@graphql-tools/delegate": "^10.2.23", + "@graphql-tools/schema": "^10.0.25", + "@graphql-tools/utils": "^10.9.1", "@whatwg-node/promise-helpers": "^1.3.0", "tslib": "^2.8.1" }, @@ -1945,10 +1941,32 @@ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.1.tgz", + "integrity": "sha512-Oau4yL24d2B5IL4ma4UpbQigkVhzPDXLoqy1ggK4gnHg/stmkffJE4oOXHXF3uz0UEpywG68KcyXsyYpA1Re/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.0", + "iconv-lite": "^0.6.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { @@ -1967,9 +1985,9 @@ } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz", - "integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==", + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, "license": "MIT", "dependencies": { @@ -1978,16 +1996,16 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2041,9 +2059,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz", - "integrity": "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", + "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", "cpu": [ "arm" ], @@ -2055,9 +2073,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.1.tgz", - "integrity": "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", + "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", "cpu": [ "arm64" ], @@ -2069,9 +2087,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.1.tgz", - "integrity": "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", + "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", "cpu": [ "arm64" ], @@ -2083,9 +2101,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.1.tgz", - "integrity": "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", + "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", "cpu": [ "x64" ], @@ -2097,9 +2115,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.1.tgz", - "integrity": "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", + "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", "cpu": [ "arm64" ], @@ -2111,9 +2129,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.1.tgz", - "integrity": "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", + "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", "cpu": [ "x64" ], @@ -2125,9 +2143,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.1.tgz", - "integrity": "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", + "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", "cpu": [ "arm" ], @@ -2139,9 +2157,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.1.tgz", - "integrity": "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", + "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", "cpu": [ "arm" ], @@ -2153,9 +2171,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.1.tgz", - "integrity": "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", + "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", "cpu": [ "arm64" ], @@ -2167,9 +2185,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.1.tgz", - "integrity": "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", + "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", "cpu": [ "arm64" ], @@ -2181,9 +2199,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.1.tgz", - "integrity": "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", + "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", "cpu": [ "loong64" ], @@ -2194,10 +2212,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.1.tgz", - "integrity": "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", + "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", "cpu": [ "ppc64" ], @@ -2209,9 +2227,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.1.tgz", - "integrity": "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", + "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", "cpu": [ "riscv64" ], @@ -2223,9 +2241,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.1.tgz", - "integrity": "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", + "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", "cpu": [ "riscv64" ], @@ -2237,9 +2255,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.1.tgz", - "integrity": "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", + "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", "cpu": [ "s390x" ], @@ -2251,9 +2269,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.1.tgz", - "integrity": "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", + "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", "cpu": [ "x64" ], @@ -2265,9 +2283,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.1.tgz", - "integrity": "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", + "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", "cpu": [ "x64" ], @@ -2279,9 +2297,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.1.tgz", - "integrity": "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", + "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", "cpu": [ "arm64" ], @@ -2293,9 +2311,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.1.tgz", - "integrity": "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", + "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", "cpu": [ "ia32" ], @@ -2307,9 +2325,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.1.tgz", - "integrity": "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", + "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", "cpu": [ "x64" ], @@ -2324,6 +2342,7 @@ "version": "0.15.3", "resolved": "https://registry.npmjs.org/@solidjs/router/-/router-0.15.3.tgz", "integrity": "sha512-iEbW8UKok2Oio7o6Y4VTzLj+KFCmQPGEpm1fS3xixwFBdclFVBvaQVeibl1jys4cujfAK5Kn6+uG2uBm3lxOMw==", + "dev": true, "license": "MIT", "peerDependencies": { "solid-js": "^1.8.6" @@ -2384,13 +2403,13 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", - "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.20.7" + "@babel/types": "^7.28.2" } }, "node_modules/@types/estree": { @@ -2408,22 +2427,15 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", - "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.8.0" + "undici-types": "~7.10.0" } }, - "node_modules/@types/prettier": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", - "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/prismjs": { "version": "1.26.5", "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", @@ -2456,13 +2468,13 @@ } }, "node_modules/@whatwg-node/fetch": { - "version": "0.10.9", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.9.tgz", - "integrity": "sha512-2TaXKmjy53cybNtaAtzbPOzwIPkjXbzvZcimnaJxQwYXKSC8iYnWoZOyT4+CFt8w0KDieg5J5dIMNzUrW/UZ5g==", + "version": "0.10.10", + "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.10.tgz", + "integrity": "sha512-watz4i/Vv4HpoJ+GranJ7HH75Pf+OkPQ63NoVmru6Srgc8VezTArB00i/oQlnn0KWh14gM42F22Qcc9SU9mo/w==", "dev": true, "license": "MIT", "dependencies": { - "@whatwg-node/node-fetch": "^0.7.22", + "@whatwg-node/node-fetch": "^0.7.25", "urlpattern-polyfill": "^10.0.0" }, "engines": { @@ -2470,9 +2482,9 @@ } }, "node_modules/@whatwg-node/node-fetch": { - "version": "0.7.22", - "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.7.22.tgz", - "integrity": "sha512-h4GGjGF2vH3kGJ/fEOeg9Xfu4ncoyRwFcjGIxr/5dTBgZNVwq888byIsZ+XXRDJnNnRlzVVVQDcqrZpY2yctGA==", + "version": "0.7.25", + "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.7.25.tgz", + "integrity": "sha512-szCTESNJV+Xd56zU6ShOi/JWROxE9IwCic8o5D9z5QECZloas6Ez5tUuKqXTAdu6fHFx1t6C+5gwj8smzOLjtg==", "dev": true, "license": "MIT", "dependencies": { @@ -2625,9 +2637,9 @@ } }, "node_modules/babel-plugin-jsx-dom-expressions": { - "version": "0.39.8", - "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.39.8.tgz", - "integrity": "sha512-/MVOIIjonylDXnrWmG23ZX82m9mtKATsVHB7zYlPfDR9Vdd/NBE48if+wv27bSkBtyO7EPMUlcUc4J63QwuACQ==", + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.40.1.tgz", + "integrity": "sha512-b4iHuirqK7RgaMzB2Lsl7MqrlDgQtVRSSazyrmx7wB3T759ggGjod5Rkok5MfHjQXhR7tRPmdwoeGPqBnW2KfA==", "dev": true, "license": "MIT", "dependencies": { @@ -2656,16 +2668,22 @@ } }, "node_modules/babel-preset-solid": { - "version": "1.9.6", - "resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.9.6.tgz", - "integrity": "sha512-HXTK9f93QxoH8dYn1M2mJdOlWgMsR88Lg/ul6QCZGkNTktjTE5HAf93YxQumHoCudLEtZrU1cFCMFOVho6GqFg==", + "version": "1.9.9", + "resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.9.9.tgz", + "integrity": "sha512-pCnxWrciluXCeli/dj5PIEHgbNzim3evtTn12snjqqg8QZWJNMjH1AWIp4iG/tbVjqQ72aBEymMSagvmgxubXw==", "dev": true, "license": "MIT", "dependencies": { - "babel-plugin-jsx-dom-expressions": "^0.39.8" + "babel-plugin-jsx-dom-expressions": "^0.40.1" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.0.0", + "solid-js": "^1.9.8" + }, + "peerDependenciesMeta": { + "solid-js": { + "optional": true + } } }, "node_modules/balanced-match": { @@ -2732,9 +2750,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", + "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", "dev": true, "funding": [ { @@ -2752,8 +2770,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", + "caniuse-lite": "^1.0.30001733", + "electron-to-chromium": "^1.5.199", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -2828,9 +2846,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "version": "1.0.30001735", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz", + "integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==", "dev": true, "funding": [ { @@ -2918,9 +2936,9 @@ } }, "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", + "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", "dev": true, "license": "MIT" }, @@ -3147,6 +3165,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, "license": "MIT" }, "node_modules/data-uri-to-buffer": { @@ -3282,9 +3301,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.191", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.191.tgz", - "integrity": "sha512-xcwe9ELcuxYLUFqZZxL19Z6HVKcvNkIwhbHUz7L3us6u12yR+7uY89dSl570f/IqNthx8dAw3tojG7i4Ni4tDA==", + "version": "1.5.203", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.203.tgz", + "integrity": "sha512-uz4i0vLhfm6dLZWbz/iH88KNDV+ivj5+2SA+utpgjKaj9Q0iDLuwk6Idhe9BTxciHudyx6IvTvijhkPvFGUQ0g==", "dev": true, "license": "ISC" }, @@ -3319,9 +3338,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", - "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3332,32 +3351,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.8", - "@esbuild/android-arm": "0.25.8", - "@esbuild/android-arm64": "0.25.8", - "@esbuild/android-x64": "0.25.8", - "@esbuild/darwin-arm64": "0.25.8", - "@esbuild/darwin-x64": "0.25.8", - "@esbuild/freebsd-arm64": "0.25.8", - "@esbuild/freebsd-x64": "0.25.8", - "@esbuild/linux-arm": "0.25.8", - "@esbuild/linux-arm64": "0.25.8", - "@esbuild/linux-ia32": "0.25.8", - "@esbuild/linux-loong64": "0.25.8", - "@esbuild/linux-mips64el": "0.25.8", - "@esbuild/linux-ppc64": "0.25.8", - "@esbuild/linux-riscv64": "0.25.8", - "@esbuild/linux-s390x": "0.25.8", - "@esbuild/linux-x64": "0.25.8", - "@esbuild/netbsd-arm64": "0.25.8", - "@esbuild/netbsd-x64": "0.25.8", - "@esbuild/openbsd-arm64": "0.25.8", - "@esbuild/openbsd-x64": "0.25.8", - "@esbuild/openharmony-arm64": "0.25.8", - "@esbuild/sunos-x64": "0.25.8", - "@esbuild/win32-arm64": "0.25.8", - "@esbuild/win32-ia32": "0.25.8", - "@esbuild/win32-x64": "0.25.8" + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" } }, "node_modules/escalade": { @@ -3380,21 +3399,6 @@ "node": ">=0.8.0" } }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "license": "MIT", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -3760,13 +3764,13 @@ } }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" @@ -3871,17 +3875,17 @@ "license": "ISC" }, "node_modules/inquirer": { - "version": "8.2.6", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", - "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "version": "8.2.7", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.7.tgz", + "integrity": "sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==", "dev": true, "license": "MIT", "dependencies": { + "@inquirer/external-editor": "^1.0.0", "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", - "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.21", "mute-stream": "0.0.8", @@ -4827,16 +4831,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5044,22 +5038,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/prismjs": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", @@ -5205,9 +5183,9 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz", - "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", + "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", "dev": true, "license": "MIT", "dependencies": { @@ -5221,26 +5199,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.45.1", - "@rollup/rollup-android-arm64": "4.45.1", - "@rollup/rollup-darwin-arm64": "4.45.1", - "@rollup/rollup-darwin-x64": "4.45.1", - "@rollup/rollup-freebsd-arm64": "4.45.1", - "@rollup/rollup-freebsd-x64": "4.45.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.45.1", - "@rollup/rollup-linux-arm-musleabihf": "4.45.1", - "@rollup/rollup-linux-arm64-gnu": "4.45.1", - "@rollup/rollup-linux-arm64-musl": "4.45.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.45.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.45.1", - "@rollup/rollup-linux-riscv64-gnu": "4.45.1", - "@rollup/rollup-linux-riscv64-musl": "4.45.1", - "@rollup/rollup-linux-s390x-gnu": "4.45.1", - "@rollup/rollup-linux-x64-gnu": "4.45.1", - "@rollup/rollup-linux-x64-musl": "4.45.1", - "@rollup/rollup-win32-arm64-msvc": "4.45.1", - "@rollup/rollup-win32-ia32-msvc": "4.45.1", - "@rollup/rollup-win32-x64-msvc": "4.45.1", + "@rollup/rollup-android-arm-eabi": "4.46.2", + "@rollup/rollup-android-arm64": "4.46.2", + "@rollup/rollup-darwin-arm64": "4.46.2", + "@rollup/rollup-darwin-x64": "4.46.2", + "@rollup/rollup-freebsd-arm64": "4.46.2", + "@rollup/rollup-freebsd-x64": "4.46.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", + "@rollup/rollup-linux-arm-musleabihf": "4.46.2", + "@rollup/rollup-linux-arm64-gnu": "4.46.2", + "@rollup/rollup-linux-arm64-musl": "4.46.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", + "@rollup/rollup-linux-ppc64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-musl": "4.46.2", + "@rollup/rollup-linux-s390x-gnu": "4.46.2", + "@rollup/rollup-linux-x64-gnu": "4.46.2", + "@rollup/rollup-linux-x64-musl": "4.46.2", + "@rollup/rollup-win32-arm64-msvc": "4.46.2", + "@rollup/rollup-win32-ia32-msvc": "4.46.2", + "@rollup/rollup-win32-x64-msvc": "4.46.2", "fsevents": "~2.3.2" } }, @@ -5349,6 +5327,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz", "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -5358,6 +5337,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.2.tgz", "integrity": "sha512-0QvCV2lM3aj/U3YozDiVwx9zpH0q8A60CTWIv4Jszj/givcudPb48B+rkU5D51NJ0pTpweGMttHjboPa9/zoIQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -5437,9 +5417,10 @@ } }, "node_modules/solid-js": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.7.tgz", - "integrity": "sha512-/saTKi8iWEM233n5OSi1YHCCuh66ZIQ7aK2hsToPe4tqGm7qAejU1SwNuTPivbWAYq7SjuHVVYxxuZQNRbICiw==", + "version": "1.9.9", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.9.tgz", + "integrity": "sha512-A0ZBPJQldAeGCTW0YRYJmt7RCeh5rbFfPZ2aOttgYnctHE7HgKeHCBB/PVc2P7eOfmNXqMFFFoYYdm3S4dcbkA==", + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.1.0", @@ -5659,11 +5640,14 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -5696,19 +5680,6 @@ "tslib": "^2.0.3" } }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5757,9 +5728,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5808,9 +5779,9 @@ } }, "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "dev": true, "license": "MIT" }, @@ -5900,9 +5871,9 @@ "license": "ISC" }, "node_modules/vite": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz", - "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.2.tgz", + "integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5910,7 +5881,7 @@ "fdir": "^6.4.6", "picomatch": "^4.0.3", "postcss": "^8.5.6", - "rollup": "^4.40.0", + "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "bin": { @@ -5975,9 +5946,9 @@ } }, "node_modules/vite-plugin-solid": { - "version": "2.11.7", - "resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.11.7.tgz", - "integrity": "sha512-5TgK1RnE449g0Ryxb9BXqem89RSy7fE8XGVCo+Gw84IHgPuPVP7nYNP6WBVAaY/0xw+OqfdQee+kusL0y3XYNg==", + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.11.8.tgz", + "integrity": "sha512-hFrCxBfv3B1BmFqnJF4JOCYpjrmi/zwyeKjcomQ0khh8HFyQ8SbuBWQ7zGojfrz6HUOBFrJBNySDi/JgAHytWg==", "dev": true, "license": "MIT", "dependencies": { @@ -5991,7 +5962,7 @@ "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "peerDependenciesMeta": { "@testing-library/jest-dom": { @@ -6000,11 +5971,14 @@ } }, "node_modules/vite/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -6150,9 +6124,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true, "license": "ISC", "bin": { diff --git a/package.json b/package.json index abc0ff4c..e48e76d3 100644 --- a/package.json +++ b/package.json @@ -19,24 +19,20 @@ "@graphql-codegen/typescript": "^4.1.6", "@graphql-codegen/typescript-operations": "^4.6.1", "@graphql-codegen/typescript-resolvers": "^4.5.1", + "@solidjs/router": "^0.15.3", "@types/node": "^24.1.0", - "@types/prettier": "^2.7.3", "@types/prismjs": "^1.26.5", "graphql": "^16.11.0", "graphql-tag": "^2.12.6", "lightningcss": "^1.30.1", - "prettier": "^3.6.2", "prismjs": "^1.30.0", "solid-js": "^1.9.7", "terser": "^5.43.0", "typescript": "^5.8.3", - "vite": "^7.0.6", + "vite": "^7.1.2", "vite-plugin-solid": "^2.11.7" }, "overrides": { - "vite": "^7.0.6" - }, - "dependencies": { - "@solidjs/router": "^0.15.3" + "vite": "^7.1.2" } } diff --git a/pyproject.toml b/pyproject.toml index 487e8340..6cff75d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -222,6 +222,7 @@ ignore = [ "UP006", # use Set as type "UP035", # use Set as type "PERF401", # list comprehension - иногда нужно + "PLC0415", # импорты не в начале файла - иногда нужно "ANN201", # Missing return type annotation for private function `wrapper` - иногда нужно ] diff --git a/services/rbac_init.py b/rbac/__init__.py similarity index 57% rename from services/rbac_init.py rename to rbac/__init__.py index 662e1b03..fb78e811 100644 --- a/services/rbac_init.py +++ b/rbac/__init__.py @@ -1,24 +1,17 @@ -""" -Модуль инициализации RBAC системы. - -Настраивает dependency injection для разрешения циклических зависимостей. -Должен вызываться при старте приложения. -""" - -from auth.rbac_interface import set_community_queries, set_rbac_operations +from rbac.interface import set_community_queries, set_rbac_operations from utils.logger import root_logger as logger def initialize_rbac() -> None: """ Инициализирует RBAC систему с dependency injection. - + Должна быть вызвана один раз при старте приложения после импорта всех модулей. """ - from services.rbac_impl import community_queries, rbac_operations + from rbac.operations import community_queries, rbac_operations # Устанавливаем реализации set_rbac_operations(rbac_operations) set_community_queries(community_queries) - + logger.info("🧿 RBAC система инициализирована с dependency injection") diff --git a/services/rbac.py b/rbac/api.py similarity index 98% rename from services/rbac.py rename to rbac/api.py index 70683fea..f2701498 100644 --- a/services/rbac.py +++ b/rbac/api.py @@ -13,8 +13,8 @@ from functools import wraps from typing import Any, Callable from auth.orm import Author -from auth.rbac_interface import get_community_queries, get_rbac_operations -from services.db import local_session +from rbac.interface import get_community_queries, get_rbac_operations +from storage.db import local_session from settings import ADMIN_EMAILS from utils.logger import root_logger as logger @@ -49,30 +49,31 @@ async def get_permissions_for_role(role: str, community_id: int) -> list[str]: async def update_all_communities_permissions() -> None: """ Обновляет права для всех существующих сообществ на основе актуальных дефолтных настроек. - + Используется в админ-панели для применения изменений в правах на все сообщества. """ rbac_ops = get_rbac_operations() - + # Поздний импорт для избежания циклических зависимостей from orm.community import Community - + try: with local_session() as session: # Получаем все сообщества communities = session.query(Community).all() - + for community in communities: # Сбрасываем кеш прав для каждого сообщества - from services.redis import redis + from storage.redis import redis + key = f"community:roles:{community.id}" await redis.execute("DEL", key) - + # Переинициализируем права с актуальными дефолтными настройками await rbac_ops.initialize_community_permissions(community.id) - + logger.info(f"Обновлены права для {len(communities)} сообществ") - + except Exception as e: logger.error(f"Ошибка при обновлении прав всех сообществ: {e}", exc_info=True) raise @@ -252,7 +253,7 @@ def get_community_id_from_context(info) -> int: return community.id logger.warning(f"[get_community_id_from_context] Сообщество с slug {slug} не найдено") except Exception as e: - logger.error(f"[get_community_id_from_context] Ошибка при поиске community_id: {e}") + logger.exception(f"[get_community_id_from_context] Ошибка при поиске community_id: {e}") # Пробуем из прямых аргументов if hasattr(info, "field_asts") and info.field_asts: diff --git a/services/default_role_permissions.json b/rbac/default_role_permissions.json similarity index 100% rename from services/default_role_permissions.json rename to rbac/default_role_permissions.json diff --git a/auth/rbac_interface.py b/rbac/interface.py similarity index 88% rename from auth/rbac_interface.py rename to rbac/interface.py index 1c02a57b..4c14b2c4 100644 --- a/auth/rbac_interface.py +++ b/rbac/interface.py @@ -5,14 +5,13 @@ не импортирует ORM модели и не создает циклических зависимостей. """ -from abc import ABC, abstractmethod from typing import Any, Protocol class RBACOperations(Protocol): """ Протокол для RBAC операций, позволяющий ORM моделям - выполнять операции с правами без прямого импорта services.rbac + выполнять операции с правами без прямого импорта rbac.api """ async def get_permissions_for_role(self, role: str, community_id: int) -> list[str]: @@ -29,9 +28,7 @@ class RBACOperations(Protocol): """Проверяет разрешение пользователя в сообществе""" ... - async def _roles_have_permission( - self, role_slugs: list[str], permission: str, community_id: int - ) -> bool: + async def _roles_have_permission(self, role_slugs: list[str], permission: str, community_id: int) -> bool: """Проверяет, есть ли у набора ролей конкретное разрешение в сообществе""" ... @@ -42,9 +39,7 @@ class CommunityAuthorQueries(Protocol): выполнять запросы без прямого импорта ORM моделей """ - def get_user_roles_in_community( - self, author_id: int, community_id: int, session: Any = None - ) -> list[str]: + def get_user_roles_in_community(self, author_id: int, community_id: int, session: Any = None) -> list[str]: """Получает роли пользователя в сообществе""" ... @@ -56,13 +51,13 @@ _community_queries: CommunityAuthorQueries | None = None def set_rbac_operations(ops: RBACOperations) -> None: """Устанавливает реализацию RBAC операций""" - global _rbac_operations + global _rbac_operations # noqa: PLW0603 _rbac_operations = ops def set_community_queries(queries: CommunityAuthorQueries) -> None: """Устанавливает реализацию запросов сообщества""" - global _community_queries + global _community_queries # noqa: PLW0603 _community_queries = queries diff --git a/services/rbac_impl.py b/rbac/operations.py similarity index 94% rename from services/rbac_impl.py rename to rbac/operations.py index 6b57cbac..6add797a 100644 --- a/services/rbac_impl.py +++ b/rbac/operations.py @@ -5,24 +5,21 @@ не импортирует ORM модели напрямую, используя dependency injection. """ -import asyncio import json from pathlib import Path from typing import Any -from auth.orm import Author -from auth.rbac_interface import CommunityAuthorQueries, RBACOperations, get_community_queries -from services.db import local_session -from services.redis import redis -from settings import ADMIN_EMAILS +from rbac.interface import CommunityAuthorQueries, RBACOperations, get_community_queries +from storage.db import local_session +from storage.redis import redis from utils.logger import root_logger as logger # --- Загрузка каталога сущностей и дефолтных прав --- -with Path("services/permissions_catalog.json").open() as f: +with Path("rbac/permissions_catalog.json").open() as f: PERMISSIONS_CATALOG = json.load(f) -with Path("services/default_role_permissions.json").open() as f: +with Path("rbac/default_role_permissions.json").open() as f: DEFAULT_ROLE_PERMISSIONS = json.load(f) role_names = list(DEFAULT_ROLE_PERMISSIONS.keys()) @@ -169,9 +166,7 @@ class RBACOperationsImpl(RBACOperations): class CommunityAuthorQueriesImpl(CommunityAuthorQueries): """Конкретная реализация запросов CommunityAuthor через поздний импорт""" - def get_user_roles_in_community( - self, author_id: int, community_id: int = 1, session: Any = None - ) -> list[str]: + def get_user_roles_in_community(self, author_id: int, community_id: int = 1, session: Any = None) -> list[str]: """ Получает роли пользователя в сообществе через новую систему CommunityAuthor """ diff --git a/auth/permissions.py b/rbac/permissions.py similarity index 100% rename from auth/permissions.py rename to rbac/permissions.py diff --git a/services/permissions_catalog.json b/rbac/permissions_catalog.json similarity index 100% rename from services/permissions_catalog.json rename to rbac/permissions_catalog.json diff --git a/resolvers/admin.py b/resolvers/admin.py index 5d35738b..17b88942 100644 --- a/resolvers/admin.py +++ b/resolvers/admin.py @@ -17,14 +17,14 @@ from orm.draft import DraftTopic from orm.reaction import Reaction from orm.shout import Shout, ShoutTopic from orm.topic import Topic, TopicFollower +from rbac.api import update_all_communities_permissions from resolvers.editor import delete_shout, update_shout from resolvers.topic import invalidate_topic_followers_cache, invalidate_topics_cache from services.admin import AdminService -from services.common_result import handle_error -from services.db import local_session -from services.rbac import update_all_communities_permissions -from services.redis import redis -from services.schema import mutation, query +from utils.common_result import handle_error +from storage.db import local_session +from storage.redis import redis +from storage.schema import mutation, query from utils.logger import root_logger as logger admin_service = AdminService() diff --git a/resolvers/auth.py b/resolvers/auth.py index 428515ad..7e469472 100644 --- a/resolvers/auth.py +++ b/resolvers/auth.py @@ -8,7 +8,7 @@ from graphql import GraphQLResolveInfo from starlette.responses import JSONResponse from services.auth import auth_service -from services.schema import mutation, query, type_author +from storage.schema import mutation, query, type_author from settings import SESSION_COOKIE_NAME from utils.logger import root_logger as logger diff --git a/resolvers/author.py b/resolvers/author.py index 1266dad9..f12357b9 100644 --- a/resolvers/author.py +++ b/resolvers/author.py @@ -21,10 +21,10 @@ from orm.community import Community, CommunityAuthor, CommunityFollower from orm.shout import Shout, ShoutAuthor from resolvers.stat import get_with_stat from services.auth import login_required -from services.common_result import CommonResult -from services.db import local_session -from services.redis import redis -from services.schema import mutation, query +from utils.common_result import CommonResult +from storage.db import local_session +from storage.redis import redis +from storage.schema import mutation, query from utils.logger import root_logger as logger DEFAULT_COMMUNITIES = [1] @@ -450,9 +450,7 @@ async def load_authors_search(_: None, info: GraphQLResolveInfo, **kwargs: Any) return [] -def get_author_id_from( - slug: str | None = None, user: str | None = None, author_id: int | None = None -) -> int | None: +def get_author_id_from(slug: str | None = None, user: str | None = None, author_id: int | None = None) -> int | None: """Get author ID from different identifiers""" try: if author_id: diff --git a/resolvers/bookmark.py b/resolvers/bookmark.py index 1bb782b5..dd07def4 100644 --- a/resolvers/bookmark.py +++ b/resolvers/bookmark.py @@ -7,9 +7,9 @@ from auth.orm import AuthorBookmark from orm.shout import Shout from resolvers.reader import apply_options, get_shouts_with_links, query_with_stat from services.auth import login_required -from services.common_result import CommonResult -from services.db import local_session -from services.schema import mutation, query +from utils.common_result import CommonResult +from storage.db import local_session +from storage.schema import mutation, query @query.field("load_shouts_bookmarked") diff --git a/resolvers/collab.py b/resolvers/collab.py index bf5ff341..94335c9d 100644 --- a/resolvers/collab.py +++ b/resolvers/collab.py @@ -4,8 +4,8 @@ from auth.orm import Author from orm.invite import Invite, InviteStatus from orm.shout import Shout from services.auth import login_required -from services.db import local_session -from services.schema import mutation +from storage.db import local_session +from storage.schema import mutation @mutation.field("accept_invite") diff --git a/resolvers/collection.py b/resolvers/collection.py index c5c61082..a64a5c81 100644 --- a/resolvers/collection.py +++ b/resolvers/collection.py @@ -6,9 +6,9 @@ from sqlalchemy.orm import joinedload from auth.decorators import editor_or_admin_required from auth.orm import Author from orm.collection import Collection, ShoutCollection -from services.db import local_session -from services.rbac import require_any_permission -from services.schema import mutation, query, type_collection +from rbac.api import require_any_permission +from storage.db import local_session +from storage.schema import mutation, query, type_collection from utils.logger import root_logger as logger diff --git a/resolvers/community.py b/resolvers/community.py index 60f47ded..7c1a6a66 100644 --- a/resolvers/community.py +++ b/resolvers/community.py @@ -7,15 +7,15 @@ from sqlalchemy import distinct, func from auth.orm import Author from orm.community import Community, CommunityAuthor, CommunityFollower from orm.shout import Shout, ShoutAuthor -from services.db import local_session -from services.rbac import ( +from rbac.api import ( RBACError, get_user_roles_from_context, require_any_permission, require_permission, roles_have_permission, ) -from services.schema import mutation, query, type_community +from storage.db import local_session +from storage.schema import mutation, query, type_community from utils.logger import root_logger as logger diff --git a/resolvers/draft.py b/resolvers/draft.py index d58e26ce..d40deca1 100644 --- a/resolvers/draft.py +++ b/resolvers/draft.py @@ -12,9 +12,9 @@ from cache.cache import ( from orm.draft import Draft, DraftAuthor, DraftTopic from orm.shout import Shout, ShoutAuthor, ShoutTopic from services.auth import login_required -from services.db import local_session +from storage.db import local_session from services.notify import notify_shout -from services.schema import mutation, query +from storage.schema import mutation, query from services.search import search_service from utils.extract_text import extract_text from utils.logger import root_logger as logger diff --git a/resolvers/editor.py b/resolvers/editor.py index 3fa80e25..86329b9a 100644 --- a/resolvers/editor.py +++ b/resolvers/editor.py @@ -19,10 +19,10 @@ from orm.topic import Topic from resolvers.follower import follow from resolvers.stat import get_with_stat from services.auth import login_required -from services.common_result import CommonResult -from services.db import local_session +from utils.common_result import CommonResult +from storage.db import local_session from services.notify import notify_shout -from services.schema import mutation, query +from storage.schema import mutation, query from services.search import search_service from utils.extract_text import extract_text from utils.logger import root_logger as logger diff --git a/resolvers/feed.py b/resolvers/feed.py index ae57e94f..4fca31f3 100644 --- a/resolvers/feed.py +++ b/resolvers/feed.py @@ -13,8 +13,8 @@ from resolvers.reader import ( query_with_stat, ) from services.auth import login_required -from services.db import local_session -from services.schema import query +from storage.db import local_session +from storage.schema import query from utils.logger import root_logger as logger diff --git a/resolvers/follower.py b/resolvers/follower.py index 979853a3..05e205db 100644 --- a/resolvers/follower.py +++ b/resolvers/follower.py @@ -16,10 +16,10 @@ from orm.community import Community, CommunityFollower from orm.shout import Shout, ShoutReactionsFollower from orm.topic import Topic, TopicFollower from services.auth import login_required -from services.db import local_session +from storage.db import local_session from services.notify import notify_follower -from services.redis import redis -from services.schema import mutation, query +from storage.redis import redis +from storage.schema import mutation, query from utils.logger import root_logger as logger diff --git a/resolvers/notifier.py b/resolvers/notifier.py index 37fb81a6..2b831ab4 100644 --- a/resolvers/notifier.py +++ b/resolvers/notifier.py @@ -17,8 +17,8 @@ from orm.notification import ( ) from orm.shout import Shout from services.auth import login_required -from services.db import local_session -from services.schema import mutation, query +from storage.db import local_session +from storage.schema import mutation, query from utils.logger import root_logger as logger diff --git a/resolvers/proposals.py b/resolvers/proposals.py index 7c112a8e..36ddb62a 100644 --- a/resolvers/proposals.py +++ b/resolvers/proposals.py @@ -3,7 +3,7 @@ from sqlalchemy import and_ from orm.rating import is_negative, is_positive from orm.reaction import Reaction, ReactionKind from orm.shout import Shout -from services.db import local_session +from storage.db import local_session from utils.diff import apply_diff, get_diff diff --git a/resolvers/rating.py b/resolvers/rating.py index 432d8adc..8a4933a0 100644 --- a/resolvers/rating.py +++ b/resolvers/rating.py @@ -8,8 +8,8 @@ from auth.orm import Author, AuthorRating from orm.reaction import Reaction, ReactionKind from orm.shout import Shout, ShoutAuthor from services.auth import login_required -from services.db import local_session -from services.schema import mutation, query +from storage.db import local_session +from storage.schema import mutation, query from utils.logger import root_logger as logger diff --git a/resolvers/reaction.py b/resolvers/reaction.py index 7b7ce87f..dc466279 100644 --- a/resolvers/reaction.py +++ b/resolvers/reaction.py @@ -21,9 +21,9 @@ from resolvers.follower import follow from resolvers.proposals import handle_proposing from resolvers.stat import update_author_stat from services.auth import add_user_role, login_required -from services.db import local_session +from storage.db import local_session from services.notify import notify_reaction -from services.schema import mutation, query +from storage.schema import mutation, query from utils.logger import root_logger as logger diff --git a/resolvers/reader.py b/resolvers/reader.py index 71c3f317..e38a14e2 100644 --- a/resolvers/reader.py +++ b/resolvers/reader.py @@ -10,8 +10,8 @@ from auth.orm import Author from orm.reaction import Reaction, ReactionKind from orm.shout import Shout, ShoutAuthor, ShoutTopic from orm.topic import Topic -from services.db import json_array_builder, json_builder, local_session -from services.schema import query +from storage.db import json_array_builder, json_builder, local_session +from storage.schema import query from services.search import SearchService, search_text from services.viewed import ViewedStorage from utils.logger import root_logger as logger diff --git a/resolvers/stat.py b/resolvers/stat.py index b7d9cf09..82b24189 100644 --- a/resolvers/stat.py +++ b/resolvers/stat.py @@ -13,7 +13,7 @@ from orm.community import Community, CommunityFollower from orm.reaction import Reaction, ReactionKind from orm.shout import Shout, ShoutAuthor, ShoutTopic from orm.topic import Topic, TopicFollower -from services.db import local_session +from storage.db import local_session from utils.logger import root_logger as logger # Type alias for queries @@ -434,9 +434,7 @@ def get_following_count(entity_type: str, entity_id: int) -> int: return 0 -def get_shouts_count( - author_id: int | None = None, topic_id: int | None = None, community_id: int | None = None -) -> int: +def get_shouts_count(author_id: int | None = None, topic_id: int | None = None, community_id: int | None = None) -> int: """Получает количество публикаций""" try: with local_session() as session: diff --git a/resolvers/topic.py b/resolvers/topic.py index b81be880..0fb47304 100644 --- a/resolvers/topic.py +++ b/resolvers/topic.py @@ -18,11 +18,11 @@ from orm.draft import DraftTopic from orm.reaction import Reaction, ReactionKind from orm.shout import Shout, ShoutAuthor, ShoutTopic from orm.topic import Topic, TopicFollower +from rbac.api import require_any_permission, require_permission from resolvers.stat import get_with_stat -from services.db import local_session -from services.rbac import require_any_permission, require_permission -from services.redis import redis -from services.schema import mutation, query +from storage.db import local_session +from storage.redis import redis +from storage.schema import mutation, query from utils.logger import root_logger as logger diff --git a/scripts/test-ci-local.sh b/scripts/test-ci-local.sh deleted file mode 100755 index 1b4b3bff..00000000 --- a/scripts/test-ci-local.sh +++ /dev/null @@ -1,119 +0,0 @@ -#!/bin/bash -""" -Локальный тест CI - запускает серверы и тесты как в GitHub Actions -""" - -set -e # Останавливаемся при ошибке - -echo "🚀 Запуск локального CI теста..." - -# Проверяем что мы в корневой папке -if [ ! -f "pyproject.toml" ]; then - echo "❌ Запустите скрипт из корневой папки проекта" - exit 1 -fi - -# Очищаем предыдущие процессы -echo "🧹 Очищаем предыдущие процессы..." -pkill -f "python dev.py" || true -pkill -f "npm run dev" || true -pkill -f "vite" || true -pkill -f "ci-server.py" || true -rm -f backend.pid frontend.pid ci-server.pid - -# Проверяем зависимости -echo "📦 Проверяем зависимости..." -if ! command -v uv &> /dev/null; then - echo "❌ uv не установлен. Установите uv: https://docs.astral.sh/uv/getting-started/installation/" - exit 1 -fi - -if ! command -v npm &> /dev/null; then - echo "❌ npm не установлен. Установите Node.js: https://nodejs.org/" - exit 1 -fi - -# Устанавливаем зависимости -echo "📥 Устанавливаем Python зависимости..." -uv sync --group dev - -echo "📥 Устанавливаем Node.js зависимости..." -cd panel -npm ci -cd .. - -# Создаем тестовую базу -echo "🗄️ Инициализируем тестовую базу..." -touch database.db -uv run python -c " -from orm.base import Base -from orm.community import Community, CommunityFollower, CommunityAuthor -from orm.draft import Draft -from orm.invite import Invite -from orm.notification import Notification -from orm.rating import Rating -from orm.reaction import Reaction -from orm.shout import Shout -from orm.topic import Topic -from services.db import get_engine -engine = get_engine() -Base.metadata.create_all(engine) -print('Test database initialized') -" - -# Запускаем серверы -echo "🚀 Запускаем серверы..." -python scripts/ci-server.py & -CI_PID=$! -echo "CI Server PID: $CI_PID" - -# Ждем готовности серверов -echo "⏳ Ждем готовности серверов..." -timeout 120 bash -c ' - while true; do - if curl -f http://localhost:8000/ > /dev/null 2>&1 && \ - curl -f http://localhost:3000/ > /dev/null 2>&1; then - echo "✅ Все серверы готовы!" - break - fi - echo "⏳ Ожидаем серверы..." - sleep 2 - done -' - -if [ $? -ne 0 ]; then - echo "❌ Таймаут ожидания серверов" - kill $CI_PID 2>/dev/null || true - exit 1 -fi - -echo "🎯 Серверы запущены! Запускаем тесты..." - -# Запускаем тесты -echo "🧪 Запускаем unit тесты..." -uv run pytest tests/ -m "not e2e" -v --tb=short - -echo "🧪 Запускаем integration тесты..." -uv run pytest tests/ -m "integration" -v --tb=short - -echo "🧪 Запускаем E2E тесты..." -uv run pytest tests/ -m "e2e" -v --tb=short - -echo "🧪 Запускаем browser тесты..." -uv run pytest tests/ -m "browser" -v --tb=short || echo "⚠️ Browser тесты завершились с ошибками" - -# Генерируем отчет о покрытии -echo "📊 Генерируем отчет о покрытии..." -uv run pytest tests/ --cov=. --cov-report=html - -echo "🎉 Все тесты завершены!" - -# Очищаем -echo "🧹 Очищаем ресурсы..." -kill $CI_PID 2>/dev/null || true -pkill -f "python dev.py" || true -pkill -f "npm run dev" || true -pkill -f "vite" || true -rm -f backend.pid frontend.pid ci-server.pid - -echo "✅ Локальный CI тест завершен!" diff --git a/services/admin.py b/services/admin.py index 416606eb..f4b693bf 100644 --- a/services/admin.py +++ b/services/admin.py @@ -14,17 +14,11 @@ from auth.orm import Author from orm.community import Community, CommunityAuthor, role_descriptions, role_names from orm.invite import Invite, InviteStatus from orm.shout import Shout -from services.db import local_session -from services.env import EnvVariable, env_manager +from storage.db import local_session +from storage.env import EnvVariable, env_manager from settings import ADMIN_EMAILS as ADMIN_EMAILS_LIST from utils.logger import root_logger as logger -# Отложенный импорт Author для избежания циклических импортов -def get_author_model(): - """Возвращает модель Author для использования в admin""" - from auth.orm import Author - return Author - class AdminService: """Сервис для админ-панели с бизнес-логикой""" @@ -59,7 +53,6 @@ class AdminService: "slug": "system", } - Author = get_author_model() author = session.query(Author).where(Author.id == author_id).first() if author: return { diff --git a/services/auth.py b/services/auth.py index 1c4b87ac..88da303a 100644 --- a/services/auth.py +++ b/services/auth.py @@ -29,8 +29,8 @@ from orm.community import ( assign_role_to_user, get_user_roles_in_community, ) -from services.db import local_session -from services.redis import redis +from storage.db import local_session +from storage.redis import redis from settings import ( ADMIN_EMAILS, SESSION_COOKIE_NAME, @@ -39,11 +39,6 @@ from settings import ( from utils.generate_slug import generate_unique_slug from utils.logger import root_logger as logger -# Author уже импортирован в начале файла -def get_author_model(): - """Возвращает модель Author для использования в auth""" - return Author - # Список разрешенных заголовков ALLOWED_HEADERS = ["Authorization", "Content-Type"] @@ -113,7 +108,6 @@ class AuthService: # Проверяем админские права через email если нет роли админа if not is_admin: with local_session() as session: - Author = get_author_model() author = session.query(Author).where(Author.id == user_id_int).first() if author and author.email in ADMIN_EMAILS.split(","): is_admin = True @@ -167,7 +161,6 @@ class AuthService: # Проверяем уникальность email with local_session() as session: - Author = get_author_model() existing_user = session.query(Author).where(Author.email == user_dict["email"]).first() if existing_user: # Если пользователь с таким email уже существует, возвращаем его @@ -180,7 +173,6 @@ class AuthService: # Проверяем уникальность slug with local_session() as session: # Добавляем суффикс, если slug уже существует - Author = get_author_model() counter = 1 unique_slug = base_slug while session.query(Author).where(Author.slug == unique_slug).first(): @@ -267,7 +259,6 @@ class AuthService: logger.info(f"Попытка регистрации для {email}") with local_session() as session: - Author = get_author_model() user = session.query(Author).where(Author.email == email).first() if user: logger.warning(f"Пользователь {email} уже существует") @@ -307,7 +298,6 @@ class AuthService: """Отправляет ссылку подтверждения на email""" email = email.lower() with local_session() as session: - Author = get_author_model() user = session.query(Author).where(Author.email == email).first() if not user: raise ObjectNotExistError("User not found") @@ -345,7 +335,6 @@ class AuthService: username = payload.get("username") with local_session() as session: - Author = get_author_model() user = session.query(Author).where(Author.id == user_id).first() if not user: logger.warning(f"Пользователь с ID {user_id} не найден") @@ -380,7 +369,6 @@ class AuthService: try: with local_session() as session: - Author = get_author_model() author = session.query(Author).where(Author.email == email).first() if not author: logger.warning(f"Пользователь {email} не найден") diff --git a/services/notify.py b/services/notify.py index b12a8f77..b1396b2f 100644 --- a/services/notify.py +++ b/services/notify.py @@ -6,8 +6,8 @@ import orjson from orm.notification import Notification from orm.reaction import Reaction from orm.shout import Shout -from services.db import local_session -from services.redis import redis +from storage.db import local_session +from storage.redis import redis from utils.logger import root_logger as logger diff --git a/services/search.py b/services/search.py index 83d24646..32cb3a7a 100644 --- a/services/search.py +++ b/services/search.py @@ -34,7 +34,7 @@ background_tasks = [] # Import Redis client if Redis caching is enabled if SEARCH_USE_REDIS: try: - from services.redis import redis + from storage.redis import redis logger.info("Redis client imported for search caching") except ImportError: diff --git a/services/viewed.py b/services/viewed.py index 2ee8748b..295d0761 100644 --- a/services/viewed.py +++ b/services/viewed.py @@ -18,8 +18,8 @@ from google.analytics.data_v1beta.types import Filter as GAFilter from auth.orm import Author from orm.shout import Shout, ShoutAuthor, ShoutTopic from orm.topic import Topic -from services.db import local_session -from services.redis import redis +from storage.db import local_session +from storage.redis import redis from utils.logger import root_logger as logger GOOGLE_KEYFILE_PATH = os.environ.get("GOOGLE_KEYFILE_PATH", "/dump/google-service.json") diff --git a/storage/__init__.py b/storage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/db.py b/storage/db.py similarity index 100% rename from services/db.py rename to storage/db.py diff --git a/services/env.py b/storage/env.py similarity index 99% rename from services/env.py rename to storage/env.py index 0250c821..faa2b30f 100644 --- a/services/env.py +++ b/storage/env.py @@ -2,7 +2,7 @@ import os from dataclasses import dataclass from typing import ClassVar -from services.redis import redis +from storage.redis import redis from utils.logger import root_logger as logger diff --git a/services/redis.py b/storage/redis.py similarity index 100% rename from services/redis.py rename to storage/redis.py diff --git a/services/schema.py b/storage/schema.py similarity index 98% rename from services/schema.py rename to storage/schema.py index ac17d2f6..ce26ada2 100644 --- a/services/schema.py +++ b/storage/schema.py @@ -9,10 +9,11 @@ from ariadne import ( load_schema_from_path, ) +from auth.orm import Author, AuthorBookmark, AuthorFollower, AuthorRating + # Импорт Author, AuthorBookmark, AuthorFollower, AuthorRating отложен для избежания циклических импортов from orm import collection, community, draft, invite, notification, reaction, shout, topic -from services.db import create_table_if_not_exists, local_session -from auth.orm import Author, AuthorBookmark, AuthorFollower, AuthorRating +from storage.db import create_table_if_not_exists, local_session # Создаем основные типы query = QueryType() diff --git a/tests/auth/test_oauth.py b/tests/auth/test_oauth.py index 1c98a25a..e91ef054 100644 --- a/tests/auth/test_oauth.py +++ b/tests/auth/test_oauth.py @@ -7,7 +7,7 @@ from starlette.responses import JSONResponse, RedirectResponse from auth.oauth import get_user_profile, oauth_callback_http, oauth_login_http from auth.orm import Author -from services.db import local_session +from storage.db import local_session # Настройка логгера logger = logging.getLogger(__name__) diff --git a/tests/conftest.py b/tests/conftest.py index 4811beb7..7f47726e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,7 @@ import asyncio from typing import Optional, Generator, AsyncGenerator from contextlib import asynccontextmanager -from services.redis import redis +from storage.redis import redis from orm.base import BaseModel as Base @@ -574,7 +574,7 @@ def mock_verify(monkeypatch): @pytest.fixture def redis_client(): """Создает Redis клиент для тестов токенов""" - from services.redis import RedisService + from storage.redis import RedisService redis_service = RedisService() return redis_service._client @@ -593,7 +593,7 @@ def mock_redis_if_unavailable(): yield except Exception: # Redis недоступен, мокаем - with patch('services.redis.RedisService') as mock_redis: + with patch('storage.redis.RedisService') as mock_redis: # Создаем базовый mock для Redis методов mock_redis.return_value.get.return_value = None mock_redis.return_value.set.return_value = True diff --git a/tests/test_admin_panel_fixes.py b/tests/test_admin_panel_fixes.py index a79bc501..d2d5abb5 100644 --- a/tests/test_admin_panel_fixes.py +++ b/tests/test_admin_panel_fixes.py @@ -11,7 +11,7 @@ from unittest.mock import patch, MagicMock from auth.orm import Author from orm.community import Community, CommunityAuthor -from services.db import local_session +from storage.db import local_session # Используем общую фикстуру из conftest.py diff --git a/tests/test_auth_fixes.py b/tests/test_auth_fixes.py index c6350a68..5eee20d8 100644 --- a/tests/test_auth_fixes.py +++ b/tests/test_auth_fixes.py @@ -13,7 +13,7 @@ from auth.internal import verify_internal_auth from auth.permissions import ContextualPermissionCheck from orm.community import Community, CommunityAuthor from auth.permissions import ContextualPermissionCheck -from services.db import local_session +from storage.db import local_session # Используем общую фикстуру из conftest.py diff --git a/tests/test_community_creator_fix.py b/tests/test_community_creator_fix.py index 6515bbac..e92589d8 100644 --- a/tests/test_community_creator_fix.py +++ b/tests/test_community_creator_fix.py @@ -18,7 +18,7 @@ from orm.community import ( assign_role_to_user, remove_role_from_user ) -from services.db import local_session +from storage.db import local_session # Используем общую фикстуру из conftest.py diff --git a/tests/test_community_rbac.py b/tests/test_community_rbac.py index 34ba445a..31c5b7b3 100644 --- a/tests/test_community_rbac.py +++ b/tests/test_community_rbac.py @@ -12,13 +12,13 @@ from unittest.mock import patch, MagicMock from auth.orm import Author from orm.community import Community, CommunityAuthor -from services.rbac import ( +from rbac.api import ( initialize_community_permissions, get_permissions_for_role, user_has_permission, roles_have_permission ) -from services.db import local_session +from storage.db import local_session @pytest.fixture diff --git a/tests/test_config.py b/tests/test_config.py index 5e99bc7c..00eddb34 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -55,8 +55,8 @@ def create_test_app(): from ariadne.asgi import GraphQL from starlette.responses import JSONResponse - from services.db import Base - from services.schema import resolvers + from storage.db import Base + from storage.schema import resolvers # Создаем движок и таблицы engine = create_engine( diff --git a/tests/test_coverage_imports.py b/tests/test_coverage_imports.py index b20f18d9..d75ff01b 100644 --- a/tests/test_coverage_imports.py +++ b/tests/test_coverage_imports.py @@ -5,18 +5,18 @@ import pytest # Импортируем все модули для покрытия import services -import services.db -import services.redis -import services.rbac +import storage.db +import storage.redis +import rbac.api import services.admin import services.auth -import services.common_result -import services.env -import services.exception +import utils.common_result +import storage.env +import utils.exception import services.notify -import services.schema +import storage.schema import services.search -import services.sentry +import utils.sentry import services.viewed import utils @@ -83,18 +83,18 @@ class TestCoverageImports: def test_services_imports(self): """Тест импорта модулей services""" assert services is not None - assert services.db is not None - assert services.redis is not None - assert services.rbac is not None + assert storage.db is not None + assert storage.redis is not None + assert rbac.api is not None assert services.admin is not None assert services.auth is not None - assert services.common_result is not None - assert services.env is not None - assert services.exception is not None + assert utils.common_result is not None + assert storage.env is not None + assert utils.exception is not None assert services.notify is not None - assert services.schema is not None + assert storage.schema is not None assert services.search is not None - assert services.sentry is not None + assert utils.sentry is not None assert services.viewed is not None def test_utils_imports(self): diff --git a/tests/test_custom_roles.py b/tests/test_custom_roles.py index 5ca06f58..4cbe2bad 100644 --- a/tests/test_custom_roles.py +++ b/tests/test_custom_roles.py @@ -5,8 +5,8 @@ import pytest import json from unittest.mock import Mock -from services.redis import redis -from services.db import local_session +from storage.redis import redis +from storage.db import local_session from orm.community import Community diff --git a/tests/test_db_coverage.py b/tests/test_db_coverage.py index e5817275..dd07c35f 100644 --- a/tests/test_db_coverage.py +++ b/tests/test_db_coverage.py @@ -6,7 +6,7 @@ import time from sqlalchemy import create_engine, Column, Integer, String, inspect from sqlalchemy.orm import declarative_base, Session -from services.db import create_table_if_not_exists, get_column_names_without_virtual, local_session +from storage.db import create_table_if_not_exists, get_column_names_without_virtual, local_session # Создаем базовую модель для тестирования Base = declarative_base() diff --git a/tests/test_drafts.py b/tests/test_drafts.py index 2c4515ed..08f56cc7 100644 --- a/tests/test_drafts.py +++ b/tests/test_drafts.py @@ -95,9 +95,9 @@ async def test_create_shout(db_session, test_author): # Мокаем local_session чтобы использовать тестовую сессию from unittest.mock import patch - from services.db import local_session + from storage.db import local_session - with patch('services.db.local_session') as mock_local_session: + with patch('storage.db.local_session') as mock_local_session: mock_local_session.return_value = db_session result = await create_draft( @@ -126,9 +126,9 @@ async def test_load_drafts(db_session): # Мокаем local_session чтобы использовать тестовую сессию from unittest.mock import patch - from services.db import local_session + from storage.db import local_session - with patch('services.db.local_session') as mock_local_session: + with patch('storage.db.local_session') as mock_local_session: mock_local_session.return_value = db_session # Вызываем резолвер напрямую diff --git a/tests/test_follow_fix.py b/tests/test_follow_fix.py index 4efd05a3..a06b2745 100644 --- a/tests/test_follow_fix.py +++ b/tests/test_follow_fix.py @@ -16,7 +16,7 @@ import sys sys.path.append(os.path.dirname(os.path.abspath(__file__))) from cache.cache import get_cached_follower_topics -from services.redis import redis +from storage.redis import redis from utils.logger import root_logger as logger diff --git a/tests/test_rbac_debug.py b/tests/test_rbac_debug.py index 1887287b..997ffa89 100644 --- a/tests/test_rbac_debug.py +++ b/tests/test_rbac_debug.py @@ -12,7 +12,7 @@ sys.path.append(os.path.dirname(os.path.abspath(__file__))) def test_rbac_import(): """Тестируем импорт RBAC модуля""" try: - from services.rbac import require_any_permission, require_permission + from rbac.api import require_any_permission, require_permission print("✅ RBAC модуль импортирован успешно") @@ -29,7 +29,7 @@ def test_rbac_import(): def test_require_permission_decorator(): """Тестируем декоратор require_permission""" try: - from services.rbac import require_permission + from rbac.api import require_permission @require_permission("test:permission") async def test_func(*args, **kwargs): diff --git a/tests/test_rbac_integration.py b/tests/test_rbac_integration.py index b9eda4a6..b2181d69 100644 --- a/tests/test_rbac_integration.py +++ b/tests/test_rbac_integration.py @@ -12,14 +12,14 @@ import json from auth.orm import Author from orm.community import Community, CommunityAuthor -from services.rbac import ( +from rbac.api import ( initialize_community_permissions, get_permissions_for_role, user_has_permission, roles_have_permission ) -from services.db import local_session -from services.redis import redis +from storage.db import local_session +from storage.redis import redis @pytest.fixture diff --git a/tests/test_rbac_system.py b/tests/test_rbac_system.py index b7c66fd2..04d5f448 100644 --- a/tests/test_rbac_system.py +++ b/tests/test_rbac_system.py @@ -10,14 +10,14 @@ from unittest.mock import patch, MagicMock from auth.orm import Author from orm.community import Community, CommunityAuthor -from services.rbac import ( +from rbac.api import ( initialize_community_permissions, get_role_permissions_for_community, get_permissions_for_role, user_has_permission, roles_have_permission ) -from services.db import local_session +from storage.db import local_session @pytest.fixture @@ -180,7 +180,7 @@ class TestRBACPermissionChecking: async def test_user_with_author_role_has_reader_permissions(self, db_session, test_users, test_community): """Тест что пользователь с ролью author имеет разрешения reader""" # Используем local_session для создания записи - from services.db import local_session + from storage.db import local_session from orm.community import CommunityAuthor with local_session() as session: @@ -214,7 +214,7 @@ class TestRBACPermissionChecking: async def test_user_with_editor_role_has_author_permissions(self, db_session, test_users, test_community): """Тест что пользователь с ролью editor имеет разрешения author""" # Используем local_session для создания записи - from services.db import local_session + from storage.db import local_session from orm.community import CommunityAuthor with local_session() as session: @@ -248,7 +248,7 @@ class TestRBACPermissionChecking: async def test_user_with_admin_role_has_all_permissions(self, db_session, test_users, test_community): """Тест что пользователь с ролью admin имеет все разрешения""" # Используем local_session для создания записи - from services.db import local_session + from storage.db import local_session from orm.community import CommunityAuthor with local_session() as session: diff --git a/tests/test_redis_coverage.py b/tests/test_redis_coverage.py index 59f628aa..f5429a95 100644 --- a/tests/test_redis_coverage.py +++ b/tests/test_redis_coverage.py @@ -9,7 +9,7 @@ import pytest import redis.asyncio as aioredis from redis.asyncio import Redis -from services.redis import ( +from storage.redis import ( RedisService, close_redis, init_redis, @@ -28,7 +28,7 @@ class TestRedisServiceInitialization: def test_redis_service_init_without_aioredis(self): """Тест инициализации без aioredis""" - with patch("services.redis.aioredis", None): + with patch("storage.redis.aioredis", None): service = RedisService() assert service._is_available is False @@ -58,7 +58,7 @@ class TestRedisConnectionManagement: """Тест успешного подключения""" service = RedisService() - with patch("services.redis.aioredis.from_url") as mock_from_url: + with patch("storage.redis.aioredis.from_url") as mock_from_url: mock_client = AsyncMock() mock_client.ping = AsyncMock(return_value=True) mock_from_url.return_value = mock_client @@ -73,7 +73,7 @@ class TestRedisConnectionManagement: """Тест неудачного подключения""" service = RedisService() - with patch("services.redis.aioredis.from_url") as mock_from_url: + with patch("storage.redis.aioredis.from_url") as mock_from_url: mock_from_url.side_effect = Exception("Connection failed") await service.connect() @@ -84,7 +84,7 @@ class TestRedisConnectionManagement: @pytest.mark.asyncio async def test_connect_without_aioredis(self): """Тест подключения без aioredis""" - with patch("services.redis.aioredis", None): + with patch("storage.redis.aioredis", None): service = RedisService() await service.connect() assert service._client is None @@ -96,7 +96,7 @@ class TestRedisConnectionManagement: mock_existing_client = AsyncMock() service._client = mock_existing_client - with patch("services.redis.aioredis.from_url") as mock_from_url: + with patch("storage.redis.aioredis.from_url") as mock_from_url: mock_client = AsyncMock() mock_client.ping = AsyncMock(return_value=True) mock_from_url.return_value = mock_client @@ -149,7 +149,7 @@ class TestRedisCommandExecution: @pytest.mark.asyncio async def test_execute_without_aioredis(self): """Тест выполнения команды без aioredis""" - with patch("services.redis.aioredis", None): + with patch("storage.redis.aioredis", None): service = RedisService() result = await service.execute("test_command") assert result is None @@ -874,7 +874,7 @@ class TestAdditionalRedisCoverage: service._client = mock_client mock_client.close.side_effect = Exception("Close error") - with patch('services.redis.aioredis.from_url') as mock_from_url: + with patch('storage.redis.aioredis.from_url') as mock_from_url: mock_new_client = AsyncMock() mock_from_url.return_value = mock_new_client diff --git a/tests/test_redis_functionality.py b/tests/test_redis_functionality.py index de308fd6..3065bb1e 100644 --- a/tests/test_redis_functionality.py +++ b/tests/test_redis_functionality.py @@ -7,7 +7,7 @@ import pytest import asyncio import json -from services.redis import RedisService +from storage.redis import RedisService class TestRedisFunctionality: diff --git a/tests/test_simple_unfollow_test.py b/tests/test_simple_unfollow_test.py index e20d90d8..bf04369a 100644 --- a/tests/test_simple_unfollow_test.py +++ b/tests/test_simple_unfollow_test.py @@ -14,7 +14,7 @@ import sys sys.path.append(os.path.dirname(os.path.abspath(__file__))) from cache.cache import get_cached_follower_topics -from services.redis import redis +from storage.redis import redis from utils.logger import root_logger as logger diff --git a/tests/test_unfollow_fix.py b/tests/test_unfollow_fix.py index e80fa888..25b0e791 100644 --- a/tests/test_unfollow_fix.py +++ b/tests/test_unfollow_fix.py @@ -17,8 +17,8 @@ sys.path.append(os.path.dirname(os.path.abspath(__file__))) from cache.cache import get_cached_follower_topics from orm.topic import Topic, TopicFollower -from services.db import local_session -from services.redis import redis +from storage.db import local_session +from storage.redis import redis from utils.logger import root_logger as logger diff --git a/tests/test_unpublish_shout.py b/tests/test_unpublish_shout.py index b06fea1c..695fea9b 100644 --- a/tests/test_unpublish_shout.py +++ b/tests/test_unpublish_shout.py @@ -22,7 +22,7 @@ from auth.orm import Author from orm.community import assign_role_to_user from orm.shout import Shout from resolvers.editor import unpublish_shout -from services.db import local_session +from storage.db import local_session # Настройка логгера logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") diff --git a/tests/test_update_security.py b/tests/test_update_security.py index f7cf0e9d..f69cccef 100644 --- a/tests/test_update_security.py +++ b/tests/test_update_security.py @@ -18,7 +18,7 @@ sys.path.append(str(Path(__file__).parent)) from auth.orm import Author from resolvers.auth import update_security -from services.db import local_session +from storage.db import local_session # Настройка логгера logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") diff --git a/services/common_result.py b/utils/common_result.py similarity index 85% rename from services/common_result.py rename to utils/common_result.py index 733ee69d..2f709131 100644 --- a/services/common_result.py +++ b/utils/common_result.py @@ -11,12 +11,6 @@ from orm.shout import Shout from orm.topic import Topic from utils.logger import root_logger as logger -# Отложенный импорт Author для избежания циклических импортов -def get_author_model(): - """Возвращает модель Author для использования в common_result""" - from auth.orm import Author - return Author - def handle_error(operation: str, error: Exception) -> GraphQLError: """Обрабатывает ошибки в резолверах""" diff --git a/utils/encoders.py b/utils/encoders.py index 4df6184e..686f001f 100644 --- a/utils/encoders.py +++ b/utils/encoders.py @@ -4,7 +4,7 @@ JSON encoders and utilities import json from datetime import date, datetime -from typing import Any, Union +from typing import Any import orjson @@ -23,7 +23,7 @@ def default_json_encoder(obj: Any) -> Any: TypeError: Если объект не может быть сериализован """ # Обработка datetime - if isinstance(obj, (datetime, date)): + if isinstance(obj, (datetime | date)): return obj.isoformat() serialized = False @@ -75,7 +75,7 @@ def orjson_dumps(obj: Any, **kwargs: Any) -> bytes: return orjson.dumps(obj, default=default_json_encoder, **kwargs) -def orjson_loads(data: Union[str, bytes]) -> Any: +def orjson_loads(data: str | bytes) -> Any: """ Десериализация объекта с помощью orjson. diff --git a/services/exception.py b/utils/exception.py similarity index 100% rename from services/exception.py rename to utils/exception.py diff --git a/utils/generate_slug.py b/utils/generate_slug.py index 0a172c39..006fdf46 100644 --- a/utils/generate_slug.py +++ b/utils/generate_slug.py @@ -2,7 +2,7 @@ import re from urllib.parse import quote_plus from auth.orm import Author -from services.db import local_session +from storage.db import local_session def replace_translit(src: str | None) -> str: diff --git a/services/sentry.py b/utils/sentry.py similarity index 84% rename from services/sentry.py rename to utils/sentry.py index 35b5ac0e..cef11631 100644 --- a/services/sentry.py +++ b/utils/sentry.py @@ -16,7 +16,7 @@ logger.setLevel(logging.DEBUG) # Более подробное логирова def start_sentry() -> None: try: - logger.info("[services.sentry] Sentry init started...") + logger.info("[utils.sentry] Sentry init started...") sentry_sdk.init( dsn=GLITCHTIP_DSN, traces_sample_rate=1.0, # Захват 100% транзакций @@ -25,6 +25,6 @@ def start_sentry() -> None: integrations=[StarletteIntegration(), AriadneIntegration(), SqlalchemyIntegration()], send_default_pii=True, # Отправка информации о пользователе (PII) ) - logger.info("[services.sentry] Sentry initialized successfully.") + logger.info("[utils.sentry] Sentry initialized successfully.") except (sentry_sdk.utils.BadDsn, ImportError, ValueError, TypeError) as _e: - logger.warning("[services.sentry] Failed to initialize Sentry", exc_info=True) + logger.warning("[utils.sentry] Failed to initialize Sentry", exc_info=True)