diff --git a/.gitignore b/.gitignore index ca647527..91500ad2 100644 --- a/.gitignore +++ b/.gitignore @@ -182,4 +182,6 @@ docs/progress/* panel/graphql/generated -test_e2e.db* \ No newline at end of file +test_e2e.db* + +uv.lock \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 276f3508..3d035d5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [0.9.29] - 2025-10-08 +## [0.9.32] - 2025-10-08 ### 🎯 Search Quality Upgrade: ColBERT + Native MUVERA + FAISS @@ -68,6 +68,69 @@ SEARCH_MODEL_TYPE=biencoder - pylate issue: https://github.com/lightonai/pylate/issues/142 - Model: `answerdotai/answerai-colbert-small-v1` +## [0.9.32] - 2025-10-05 + +### ✨ Features +- **Π Π΅Π΄Π°ΠΊΡ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΌΠΈΠ³Ρ€ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Ρ… ΡˆΠ°ΡƒΡ‚ΠΎΠ²**: Π”ΠΎΠ±Π°Π²Π»Π΅Π½Π° мутация `create_draft_from_shout` для создания Ρ‡Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊΠ° ΠΈΠ· ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰Π΅Π³ΠΎ ΠΎΠΏΡƒΠ±Π»ΠΈΠΊΠΎΠ²Π°Π½Π½ΠΎΠ³ΠΎ ΡˆΠ°ΡƒΡ‚Π° + - Π‘ΠΎΠ·Π΄Π°Ρ‘Ρ‚ Ρ‡Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊ со всСми Π΄Π°Π½Π½Ρ‹ΠΌΠΈ ΠΈΠ· ΡˆΠ°ΡƒΡ‚Π° (title, body, lead, topics, authors, media, etc.) + - ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅Ρ‚ авторство ΠΏΠ΅Ρ€Π΅Π΄ созданиСм Ρ‡Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊΠ° + - ΠŸΠ΅Ρ€Π΅ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠΉ Ρ‡Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊ Ссли ΠΎΠ½ ΡƒΠΆΠ΅ создан для этого ΡˆΠ°ΡƒΡ‚Π° + - ΠšΠΎΠΏΠΈΡ€ΡƒΠ΅Ρ‚ всС связи: Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² ΠΈ Ρ‚Π΅ΠΌΡ‹ (Π²ΠΊΠ»ΡŽΡ‡Π°Ρ main topic) + +### πŸ”§ Fixed +- **NotificationEntity enum**: Π˜ΡΠΏΡ€Π°Π²Π»Π΅Π½Π° ошибка `NotificationEntity.FOLLOWER` β†’ `NotificationEntity.AUTHOR` + - Π’ enum Π½Π΅ Π±Ρ‹Π»ΠΎ значСния `FOLLOWER`, ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ `AUTHOR` для ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ ΠΎ подписчиках + +### Technical Details +- `core/schema/mutation.graphql`: Π΄ΠΎΠ±Π°Π²Π»Π΅Π½Π° мутация `create_draft_from_shout(shout_id: Int!): CommonResult!` +- `core/resolvers/draft.py`: Π΄ΠΎΠ±Π°Π²Π»Π΅Π½ resolver `create_draft_from_shout` с Π²Π°Π»ΠΈΠ΄Π°Ρ†ΠΈΠ΅ΠΉ авторства +- `core/resolvers/notifier.py`: исправлСно использованиС `NotificationEntity.AUTHOR` вмСсто Π½Π΅ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰Π΅Π³ΠΎ `FOLLOWER` + +## [0.9.31] - 2025-10-04 + +### βœ… Fixed: Notifications TODOs +- **УвСдомлСния ΠΎ followers**: Π”ΠΎΠ±Π°Π²Π»Π΅Π½Π° ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ ΠΎ подписчиках Π² `notifications_seen_thread` + - Π’Π΅ΠΏΠ΅Ρ€ΡŒ ΠΏΡ€ΠΈ ΠΊΠ»ΠΈΠΊΠ΅ Π½Π° Π³Ρ€ΡƒΠΏΠΏΡƒ "followers" всС увСдомлСния ΠΎ подписках ΠΏΠΎΠΌΠ΅Ρ‡Π°ΡŽΡ‚ΡΡ ΠΊΠ°ΠΊ ΠΏΡ€ΠΎΡ‡ΠΈΡ‚Π°Π½Π½Ρ‹Π΅ + - Π˜ΡΠΏΡ€Π°Π²Π»Π΅Π½Π° ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° thread ID `"followers"` ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎ ΠΎΡ‚ shout/reaction threads +- **УвСдомлСния ΠΎ Π½ΠΎΠ²Ρ‹Ρ… публикациях**: Π”ΠΎΠ±Π°Π²Π»Π΅Π½Π° ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ ΠΎ Π½ΠΎΠ²Ρ‹Ρ… shouts Π² `notifications_seen_thread` + - ΠŸΡ€ΠΈ ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚ΠΈΠΈ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ увСдомлСния ΠΎ Π½Π΅ΠΉ Ρ‚ΠΎΠΆΠ΅ ΠΏΠΎΠΌΠ΅Ρ‡Π°ΡŽΡ‚ΡΡ ΠΊΠ°ΠΊ ΠΏΡ€ΠΎΡ‡ΠΈΡ‚Π°Π½Π½Ρ‹Π΅ + - Π˜ΡΠΏΡ€Π°Π²Π»Π΅Π½Π° Π»ΠΎΠ³ΠΈΠΊΠ° парсинга thread ID для ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠΈ Ρ€Π°Π·Π½Ρ‹Ρ… Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ΠΎΠ² +- **Code Quality**: Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Π½Ρ‹ enum константы (`NotificationAction`, `NotificationEntity`) вмСсто строк +- **Π£Π±Ρ€Π°Π½Ρ‹ ΡƒΡΡ‚Π°Ρ€Π΅Π²ΡˆΠΈΠ΅ TODO**: Π£Π΄Π°Π»Π΅Π½ TODO ΠΏΡ€ΠΎ `notification_id` ΠΊΠ°ΠΊ offset (тСкущая Π»ΠΎΠ³ΠΈΠΊΠ° с timestamp Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚ ΠΊΠΎΡ€Ρ€Π΅ΠΊΡ‚Π½ΠΎ) + +### Technical Details +- `core/resolvers/notifier.py`: Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½Π° функция `notifications_seen_thread` для ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠΈ всСх Ρ‚ΠΈΠΏΠΎΠ² ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ +- Π”ΠΎΠ±Π°Π²Π»Π΅Π½Π° ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° `thread == "followers"` для ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ ΠΎ подписках +- Π”ΠΎΠ±Π°Π²Π»Π΅Π½Π° ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° `NotificationEntity.SHOUT` для ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ ΠΎ Π½ΠΎΠ²Ρ‹Ρ… публикациях +- Π£Π»ΡƒΡ‡ΡˆΠ΅Π½Π° ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ошибок с `logger.warning()` вмСсто ΠΈΡΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠΉ + +## [0.9.30] - 2025-10-02 + +### πŸ”§ Fixed +- **РСвалидация кСша featured ΠΌΠ°Ρ‚Π΅Ρ€ΠΈΠ°Π»ΠΎΠ²**: ΠšΡ€ΠΈΡ‚ΠΈΡ‡Π΅ΡΠΊΠΎΠ΅ исправлСниС ΠΈΠ½Π²Π°Π»ΠΈΠ΄Π°Ρ†ΠΈΠΈ кСша ΠΏΡ€ΠΈ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΈ featured статуса + - Π”ΠΎΠ±Π°Π²Π»Π΅Π½Ρ‹ ΠΊΠ»ΡŽΡ‡ΠΈ кСша для featured ΠΌΠ°Ρ‚Π΅Ρ€ΠΈΠ°Π»ΠΎΠ² Π² `invalidate_shout_related_cache` + - Π˜ΡΠΏΡ€Π°Π²Π»Π΅Π½Π° функция `set_featured`: Π΄ΠΎΠ±Π°Π²Π»Π΅Π½Π° инвалидация кСша Π»Π΅Π½Ρ‚ + - Π˜ΡΠΏΡ€Π°Π²Π»Π΅Π½Π° функция `set_unfeatured`: Π΄ΠΎΠ±Π°Π²Π»Π΅Π½Π° инвалидация кСша Π»Π΅Π½Ρ‚ + - Π’Π΅ΠΏΠ΅Ρ€ΡŒ ΠΌΠ°Ρ‚Π΅Ρ€ΠΈΠ°Π»Ρ‹ ΠΊΠΎΡ€Ρ€Π΅ΠΊΡ‚Π½ΠΎ ΠΏΠΎΡΠ²Π»ΡΡŽΡ‚ΡΡ/ΠΈΡΡ‡Π΅Π·Π°ΡŽΡ‚ с Π³Π»Π°Π²Π½ΠΎΠΉ страницы ΠΏΡ€ΠΈ Ρ„ΠΈΡ‡Π΅Ρ€ΠΈΠ½Π³Π΅/расфичСрингС + - Π£Π»ΡƒΡ‡ΡˆΠ΅Π½Π° ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ Ρ‡Π΅Ρ€Π΅Π· Π°ΡΠΈΠ½Ρ…Ρ€ΠΎΠ½Π½ΡƒΡŽ ΠΈΠ½Π²Π°Π»ΠΈΠ΄Π°Ρ†ΠΈΡŽ кСша + +### βœ… Code Quality +- **Python Standards Compliance**: Код соотвСтствуСт стандартам 003-python-standards.mdc + - ΠŸΡ€ΠΎΠΉΠ΄Π΅Π½Ρ‹ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ Ruff (linting & formatting) + - ΠŸΡ€ΠΎΠΉΠ΄Π΅Π½Ρ‹ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ MyPy (type checking) + - ВсС Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ ΠΈΠΌΠ΅ΡŽΡ‚ Ρ‚ΠΈΠΏΡ‹ ΠΈ докстринги + - ВСсты проходят ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎ + +## [0.9.29] - 2025-10-01 + +### πŸ”§ Fixed +- **Π€ΠΈΡ‡Π΅Ρ€Π΅Π½ΠΈΠ΅ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΉ**: Π˜ΡΠΏΡ€Π°Π²Π»Π΅Π½Π° Π»ΠΎΠ³ΠΈΠΊΠ° автоматичСского фичСрСния/расфичСрСния + - Π’Π΅ΠΏΠ΅Ρ€ΡŒ ΡƒΡ‡ΠΈΡ‚Ρ‹Π²Π°ΡŽΡ‚ΡΡ всС ΠΏΠΎΠ»ΠΎΠΆΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ Ρ€Π΅Π°ΠΊΡ†ΠΈΠΈ (LIKE, ACCEPT, PROOF), Π° Π½Π΅ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ LIKE + - Π˜ΡΠΏΡ€Π°Π²Π»Π΅Π½ подсчСт Ρ€Π΅Π°ΠΊΡ†ΠΈΠΉ Π² `check_to_unfeature`: ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ POSITIVE + NEGATIVE вмСсто Ρ‚ΠΎΠ»ΡŒΠΊΠΎ RATING_REACTIONS + - Π”ΠΎΠ±Π°Π²Π»Π΅Π½Π° явная ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° `reply_to.is_(None)` для ΠΈΡΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠ΅Π² + - **РСвалидация кСша**: Π”ΠΎΠ±Π°Π²Π»Π΅Π½Π° рСвалидация кСша ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΉ, Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² ΠΈ Ρ‚Π΅ΠΌ ΠΏΡ€ΠΈ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΈ `featured_at` + - Π£Π»ΡƒΡ‡ΡˆΠ΅Π½ΠΎ Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ для ΠΎΡ‚Π»Π°Π΄ΠΊΠΈ процСсса фичСрСния + ## [0.9.28] - 2025-09-28 ### πŸͺ CRITICAL Cross-Origin Auth diff --git a/README.md b/README.md index 73535fe4..1707fa69 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ core/ ### Environment Variables - `DATABASE_URL` - Database connection string - `REDIS_URL` - Redis connection string -- `JWT_SECRET` - JWT signing secret +- `JWT_SECRET_KEY` - JWT signing secret - `OAUTH_*` - OAuth provider credentials ### Database diff --git a/auth/oauth.py b/auth/oauth.py index 468d267d..cba3ab6f 100644 --- a/auth/oauth.py +++ b/auth/oauth.py @@ -819,16 +819,16 @@ async def oauth_callback_http(request: Request) -> JSONResponse | RedirectRespon token_data["client_secret"] = client.client_secret async with httpx.AsyncClient() as http_client: - response = await http_client.post( + token_response = await http_client.post( token_endpoint, data=token_data, headers={"Accept": "application/json"} ) - if response.status_code != 200: - error_msg = f"Token request failed: {response.status_code} - {response.text}" + if token_response.status_code != 200: + error_msg = f"Token request failed: {token_response.status_code} - {token_response.text}" logger.error(f"❌ {error_msg}") raise ValueError(error_msg) - token = response.json() + token = token_response.json() else: # ΠŸΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€Ρ‹ с PKCE ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠΎΠΉ code_verifier = oauth_data.get("code_verifier") diff --git a/biome.json b/biome.json index be4dc76c..6bdd0ee6 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json", + "$schema": "https://biomejs.dev/schemas/2.2.5/schema.json", "files": { "includes": [ "**/*.tsx", diff --git a/cache/cache.py b/cache/cache.py index 9074bd15..183aff36 100644 --- a/cache/cache.py +++ b/cache/cache.py @@ -513,6 +513,10 @@ async def invalidate_shout_related_cache(shout: Shout, author_id: int) -> None: "unrated", # Π½Π΅ΠΎΡ†Π΅Π½Π΅Π½Π½Ρ‹Π΅ "recent", # послСдниС "coauthored", # совмСстныС + # πŸ”§ ДобавляСм ΠΊΠ»ΡŽΡ‡ΠΈ с featured ΠΌΠ°Ρ‚Π΅Ρ€ΠΈΠ°Π»Π°ΠΌΠΈ + "featured", # featured ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ + "featured:recent", # Π½Π΅Π΄Π°Π²Π½ΠΈΠ΅ featured + "featured:top", # Ρ‚ΠΎΠΏ featured } # ДобавляСм ΠΊΠ»ΡŽΡ‡ΠΈ Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² @@ -523,6 +527,12 @@ async def invalidate_shout_related_cache(shout: Shout, author_id: int) -> None: cache_keys.update(f"topic_{t.id}" for t in shout.topics) cache_keys.update(f"topic_shouts_{t.id}" for t in shout.topics) + # πŸ”§ ДобавляСм ΠΊΠ»ΡŽΡ‡ΠΈ featured ΠΌΠ°Ρ‚Π΅Ρ€ΠΈΠ°Π»ΠΎΠ² для ΠΊΠ°ΠΆΠ΄ΠΎΠΉ Ρ‚Π΅ΠΌΡ‹ + for topic in shout.topics: + cache_keys.update( + [f"topic_{topic.id}:featured", f"topic_{topic.id}:featured:recent", f"topic_{topic.id}:featured:top"] + ) + await invalidate_shouts_cache(list(cache_keys)) diff --git a/docs/auth/README.md b/docs/auth/README.md index e6513d08..84ffd033 100644 --- a/docs/auth/README.md +++ b/docs/auth/README.md @@ -242,7 +242,7 @@ SESSION_COOKIE_SAMESITE=lax SESSION_COOKIE_MAX_AGE=2592000 # 30 Π΄Π½Π΅ΠΉ # JWT -JWT_SECRET=your_jwt_secret_key +JWT_SECRET_KEY=your_jwt_secret_key JWT_EXPIRATION_HOURS=720 # 30 Π΄Π½Π΅ΠΉ # Redis diff --git a/docs/auth/microservices.md b/docs/auth/microservices.md index 73d60f00..8ca8e59c 100644 --- a/docs/auth/microservices.md +++ b/docs/auth/microservices.md @@ -520,7 +520,7 @@ async def test_redis_integration(): ### ΠŸΠΎΠ΄Π³ΠΎΡ‚ΠΎΠ²ΠΊΠ° - [ ] НастроСн Redis connection pool с Ρ‚Π΅ΠΌΠΈ ΠΆΠ΅ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Π°ΠΌΠΈ - [ ] УстановлСны зависимости: `auth.tokens.*`, `auth.utils` -- [ ] НастроСны environment variables (JWT_SECRET, REDIS_URL) +- [ ] НастроСны environment variables (JWT_SECRET_KEY, REDIS_URL) ### РСализация - [ ] Π Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Π½Π° функция извлСчСния Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ² ΠΈΠ· запросов diff --git a/docs/auth/security.md b/docs/auth/security.md index 9ba5f477..dae54a71 100644 --- a/docs/auth/security.md +++ b/docs/auth/security.md @@ -22,7 +22,7 @@ ```python # settings.py JWT_ALGORITHM = "HS256" # HMAC with SHA-256 -JWT_SECRET = os.getenv("JWT_SECRET") # ΠœΠΈΠ½ΠΈΠΌΡƒΠΌ 256 Π±ΠΈΡ‚ +JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY") # ΠœΠΈΠ½ΠΈΠΌΡƒΠΌ 256 Π±ΠΈΡ‚ JWT_EXPIRATION_DELTA = 30 * 24 * 60 * 60 # 30 Π΄Π½Π΅ΠΉ ``` @@ -439,7 +439,7 @@ async def detect_anomalies(user_id: str, event_type: str, ip_address: str): ### Environment Variables ```bash # JWT Security -JWT_SECRET=your_super_secret_key_minimum_256_bits +JWT_SECRET_KEY=your_super_secret_key_minimum_256_bits JWT_ALGORITHM=HS256 JWT_EXPIRATION_HOURS=720 diff --git a/docs/auth/sessions.md b/docs/auth/sessions.md index 3f9a5bad..73a0434b 100644 --- a/docs/auth/sessions.md +++ b/docs/auth/sessions.md @@ -281,7 +281,7 @@ async def delete_session(token: str) -> bool: ### JWT Ρ‚ΠΎΠΊΠ΅Π½Ρ‹ - **Алгоритм**: HS256 -- **Secret**: Из ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎΠΉ окруТСния JWT_SECRET +- **Secret**: Из ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎΠΉ окруТСния JWT_SECRET_KEY - **Payload**: `{user_id, username, iat, exp}` - **Expiration**: 30 Π΄Π½Π΅ΠΉ (настраиваСтся) diff --git a/docs/auth/setup.md b/docs/auth/setup.md index 7ddad4f8..ce07b437 100644 --- a/docs/auth/setup.md +++ b/docs/auth/setup.md @@ -6,7 +6,7 @@ ```bash # JWT настройки -JWT_SECRET=your_super_secret_key_minimum_256_bits +JWT_SECRET_KEY=your_super_secret_key_minimum_256_bits JWT_ALGORITHM=HS256 JWT_EXPIRATION_HOURS=720 # 30 Π΄Π½Π΅ΠΉ @@ -69,7 +69,7 @@ LOCKOUT_DURATION=1800 # 30 ΠΌΠΈΠ½ΡƒΡ‚ # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Ρ… окруТСния python -c " import os -required = ['JWT_SECRET', 'REDIS_URL', 'GOOGLE_CLIENT_ID'] +required = ['JWT_SECRET_KEY', 'REDIS_URL', 'GOOGLE_CLIENT_ID'] for var in required: print(f'{var}: {\"βœ…\" if os.getenv(var) else \"❌\"}')" @@ -213,7 +213,7 @@ ab -n 1000 -c 10 -p login.json -T application/json http://localhost:8000/graphql ### Docker ```dockerfile # Dockerfile -ENV JWT_SECRET=your_secret_here +ENV JWT_SECRET_KEY=your_secret_here ENV REDIS_URL=redis://redis:6379/0 ENV SESSION_COOKIE_SECURE=true ``` @@ -221,8 +221,8 @@ ENV SESSION_COOKIE_SECURE=true ### Dokku/Heroku ```bash # Установка ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Ρ… окруТСния -dokku config:set myapp JWT_SECRET=xxx REDIS_URL=yyy -heroku config:set JWT_SECRET=xxx REDIS_URL=yyy +dokku config:set myapp JWT_SECRET_KEY=xxx REDIS_URL=yyy +heroku config:set JWT_SECRET_KEY=xxx REDIS_URL=yyy ``` ### Nginx настройки diff --git a/docs/auth/system.md b/docs/auth/system.md index 02d7090b..56db9839 100644 --- a/docs/auth/system.md +++ b/docs/auth/system.md @@ -330,7 +330,7 @@ OAuth Π΄Π°Π½Π½Ρ‹Π΅ хранятся Π² JSON ΠΏΠΎΠ»Π΅ `oauth` ΠΌΠΎΠ΄Π΅Π»ΠΈ `Autho ### ΠŸΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Π΅ окруТСния ```bash # JWT настройки -JWT_SECRET=your_super_secret_key +JWT_SECRET_KEY=your_super_secret_key JWT_EXPIRATION_HOURS=720 # 30 Π΄Π½Π΅ΠΉ # Redis ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ diff --git a/docs/auth/testing.md b/docs/auth/testing.md index f73bb7d0..7bd82ebf 100644 --- a/docs/auth/testing.md +++ b/docs/auth/testing.md @@ -819,7 +819,7 @@ jobs: pytest tests/auth/e2e/ -m e2e env: REDIS_URL: redis://localhost:6379/0 - JWT_SECRET: test_secret_key_for_ci + JWT_SECRET_KEY: test_secret_key_for_ci - name: Upload coverage uses: codecov/codecov-action@v3 diff --git a/docs/redis-schema.md b/docs/redis-schema.md index fae5f914..0fe4f373 100644 --- a/docs/redis-schema.md +++ b/docs/redis-schema.md @@ -127,7 +127,7 @@ env_vars:{variable_name} # STRING - Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½ ### ΠŸΡ€ΠΈΠΌΠ΅Ρ€Ρ‹ ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Ρ… ```redis -GET env_vars:JWT_SECRET # Π‘Π΅ΠΊΡ€Π΅Ρ‚Π½Ρ‹ΠΉ ΠΊΠ»ΡŽΡ‡ JWT +GET env_vars:JWT_SECRET_KEY # Π‘Π΅ΠΊΡ€Π΅Ρ‚Π½Ρ‹ΠΉ ΠΊΠ»ΡŽΡ‡ JWT GET env_vars:REDIS_URL # URL Redis GET env_vars:OAUTH_GOOGLE_CLIENT_ID # Google OAuth Client ID GET env_vars:FEATURE_REGISTRATION # Π€Π»Π°Π³ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ рСгистрации @@ -135,7 +135,7 @@ GET env_vars:FEATURE_REGISTRATION # Π€Π»Π°Π³ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ рСгистра **ΠšΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΠΈ ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Ρ…**: - **database**: DB_URL, POSTGRES_* -- **auth**: JWT_SECRET, OAUTH_* +- **auth**: JWT_SECRET_KEY, OAUTH_* - **redis**: REDIS_URL, REDIS_HOST, REDIS_PORT - **search**: SEARCH_* - **integrations**: GOOGLE_ANALYTICS_ID, SENTRY_DSN, SMTP_* diff --git a/orm/notification.py b/orm/notification.py index df6cdbbf..a64a4710 100644 --- a/orm/notification.py +++ b/orm/notification.py @@ -99,6 +99,23 @@ class NotificationSeen(Base): ) +class NotificationUnsubscribe(Base): + """МодСль для хранСния отписок ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ ΠΎΡ‚ ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ ΠΏΠΎ ΠΎΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½Π½Ρ‹ΠΌ thread_id.""" + + __tablename__ = "notification_unsubscribe" + + author_id: Mapped[int] = mapped_column(ForeignKey("author.id"), nullable=False) + thread_id: Mapped[str] = mapped_column(String, nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) + + __table_args__ = ( + PrimaryKeyConstraint(author_id, thread_id), + Index("idx_notification_unsubscribe_author", "author_id"), + Index("idx_notification_unsubscribe_thread", "thread_id"), + {"extend_existing": True}, + ) + + class Notification(Base): __tablename__ = "notification" diff --git a/package-lock.json b/package-lock.json index fe0814d5..a297be46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,32 +1,32 @@ { "name": "publy-panel", - "version": "0.9.28", + "version": "0.9.30", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "publy-panel", - "version": "0.9.28", + "version": "0.9.30", "devDependencies": { - "@biomejs/biome": "^2.2.4", + "@biomejs/biome": "^2.2.5", "@graphql-codegen/cli": "^6.0.0", - "@graphql-codegen/client-preset": "^5.0.1", + "@graphql-codegen/client-preset": "^5.0.2", "@graphql-codegen/introspection": "^5.0.0", - "@graphql-codegen/typescript": "^5.0.0", - "@graphql-codegen/typescript-operations": "^5.0.0", - "@graphql-codegen/typescript-resolvers": "^5.0.0", + "@graphql-codegen/typescript": "^5.0.1", + "@graphql-codegen/typescript-operations": "^5.0.1", + "@graphql-codegen/typescript-resolvers": "^5.0.1", "@solidjs/router": "^0.15.3", - "@types/node": "^24.5.2", + "@types/node": "^24.6.2", "@types/prismjs": "^1.26.5", "graphql": "^16.11.0", "graphql-tag": "^2.12.6", - "lightningcss": "^1.30.1", + "lightningcss": "^1.30.2", "prismjs": "^1.30.0", "solid-js": "^1.9.9", "terser": "^5.44.0", - "typescript": "^5.9.2", - "vite": "^7.1.7", - "vite-plugin-solid": "^2.11.7" + "typescript": "^5.9.3", + "vite": "^7.1.9", + "vite-plugin-solid": "^2.11.9" } }, "node_modules/@ardatan/relay-compiler": { @@ -347,9 +347,9 @@ } }, "node_modules/@biomejs/biome": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.2.4.tgz", - "integrity": "sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg==", + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.2.5.tgz", + "integrity": "sha512-zcIi+163Rc3HtyHbEO7CjeHq8DjQRs40HsGbW6vx2WI0tg8mYQOPouhvHSyEnCBAorfYNnKdR64/IxO7xQ5faw==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -363,20 +363,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.2.4", - "@biomejs/cli-darwin-x64": "2.2.4", - "@biomejs/cli-linux-arm64": "2.2.4", - "@biomejs/cli-linux-arm64-musl": "2.2.4", - "@biomejs/cli-linux-x64": "2.2.4", - "@biomejs/cli-linux-x64-musl": "2.2.4", - "@biomejs/cli-win32-arm64": "2.2.4", - "@biomejs/cli-win32-x64": "2.2.4" + "@biomejs/cli-darwin-arm64": "2.2.5", + "@biomejs/cli-darwin-x64": "2.2.5", + "@biomejs/cli-linux-arm64": "2.2.5", + "@biomejs/cli-linux-arm64-musl": "2.2.5", + "@biomejs/cli-linux-x64": "2.2.5", + "@biomejs/cli-linux-x64-musl": "2.2.5", + "@biomejs/cli-win32-arm64": "2.2.5", + "@biomejs/cli-win32-x64": "2.2.5" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.2.4.tgz", - "integrity": "sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA==", + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.2.5.tgz", + "integrity": "sha512-MYT+nZ38wEIWVcL5xLyOhYQQ7nlWD0b/4mgATW2c8dvq7R4OQjt/XGXFkXrmtWmQofaIM14L7V8qIz/M+bx5QQ==", "cpu": [ "arm64" ], @@ -391,9 +391,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.2.4.tgz", - "integrity": "sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg==", + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.2.5.tgz", + "integrity": "sha512-FLIEl73fv0R7dI10EnEiZLw+IMz3mWLnF95ASDI0kbx6DDLJjWxE5JxxBfmG+udz1hIDd3fr5wsuP7nwuTRdAg==", "cpu": [ "x64" ], @@ -408,9 +408,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.2.4.tgz", - "integrity": "sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw==", + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.2.5.tgz", + "integrity": "sha512-5DjiiDfHqGgR2MS9D+AZ8kOfrzTGqLKywn8hoXpXXlJXIECGQ32t+gt/uiS2XyGBM2XQhR6ztUvbjZWeccFMoQ==", "cpu": [ "arm64" ], @@ -425,9 +425,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.4.tgz", - "integrity": "sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ==", + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.5.tgz", + "integrity": "sha512-5Ov2wgAFwqDvQiESnu7b9ufD1faRa+40uwrohgBopeY84El2TnBDoMNXx6iuQdreoFGjwW8vH6k68G21EpNERw==", "cpu": [ "arm64" ], @@ -442,9 +442,9 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.2.4.tgz", - "integrity": "sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ==", + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.2.5.tgz", + "integrity": "sha512-fq9meKm1AEXeAWan3uCg6XSP5ObA6F/Ovm89TwaMiy1DNIwdgxPkNwxlXJX8iM6oRbFysYeGnT0OG8diCWb9ew==", "cpu": [ "x64" ], @@ -459,9 +459,9 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.4.tgz", - "integrity": "sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg==", + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.5.tgz", + "integrity": "sha512-AVqLCDb/6K7aPNIcxHaTQj01sl1m989CJIQFQEaiQkGr2EQwyOpaATJ473h+nXDUuAcREhccfRpe/tu+0wu0eQ==", "cpu": [ "x64" ], @@ -476,9 +476,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.2.4.tgz", - "integrity": "sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ==", + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.2.5.tgz", + "integrity": "sha512-xaOIad4wBambwJa6mdp1FigYSIF9i7PCqRbvBqtIi9y29QtPVQ13sDGtUnsRoe6SjL10auMzQ6YAe+B3RpZXVg==", "cpu": [ "arm64" ], @@ -493,9 +493,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.4.tgz", - "integrity": "sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg==", + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.5.tgz", + "integrity": "sha512-F/jhuXCssPFAuciMhHKk00xnCAxJRS/pUzVfXYmOMUp//XW7mO6QeCjsjvnm8L4AO/dG2VOB0O+fJPiJ2uXtIw==", "cpu": [ "x64" ], @@ -1088,21 +1088,21 @@ } }, "node_modules/@graphql-codegen/client-preset": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-5.0.1.tgz", - "integrity": "sha512-3dXS7Sh/AkV+Ewq/HB1DSCb0tZBOIdTL8zkGQjRKWaf14x21h2f/xKl2zhRh6KlXjcCrIpX+AxHAhQxs6cXwVw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-5.0.2.tgz", + "integrity": "sha512-lBkVMz7QA7FHWb71BcNB/tFFOh0LDNCPIBaJ70Lj1SIPjOfCEYmbkK6D5piPZu87m60hyWN3XDwNHEH8eGoXNA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.20.2", "@babel/template": "^7.20.7", "@graphql-codegen/add": "^6.0.0", - "@graphql-codegen/gql-tag-operations": "5.0.0", + "@graphql-codegen/gql-tag-operations": "5.0.1", "@graphql-codegen/plugin-helpers": "^6.0.0", - "@graphql-codegen/typed-document-node": "^6.0.0", - "@graphql-codegen/typescript": "^5.0.0", - "@graphql-codegen/typescript-operations": "^5.0.0", - "@graphql-codegen/visitor-plugin-common": "^6.0.0", + "@graphql-codegen/typed-document-node": "^6.0.1", + "@graphql-codegen/typescript": "^5.0.1", + "@graphql-codegen/typescript-operations": "^5.0.1", + "@graphql-codegen/visitor-plugin-common": "^6.0.1", "@graphql-tools/documents": "^1.0.0", "@graphql-tools/utils": "^10.0.0", "@graphql-typed-document-node/core": "3.2.0", @@ -1155,14 +1155,14 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/gql-tag-operations": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-5.0.0.tgz", - "integrity": "sha512-kC2pc/tyzVc1laZtlfuQHqYxF4UqB4YXzAboFfeY1cxrxCh/+H70jHnfA1O4vhPndiRd+XZA8wxPv0hIqDXYaA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-5.0.1.tgz", + "integrity": "sha512-GVd/B6mtRAXg6UxgeO805P7VDrCmVIb6qIMrE7O69j8e4EqIt/URdmJ7On+Bn8IIKp7TcpcLSo/VI28ptcssNw==", "dev": true, "license": "MIT", "dependencies": { "@graphql-codegen/plugin-helpers": "^6.0.0", - "@graphql-codegen/visitor-plugin-common": "6.0.0", + "@graphql-codegen/visitor-plugin-common": "6.0.1", "@graphql-tools/utils": "^10.0.0", "auto-bind": "~4.0.0", "tslib": "~2.6.0" @@ -1260,14 +1260,14 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/typed-document-node": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-6.0.0.tgz", - "integrity": "sha512-OYmbadwvjq19yCZjioy901pLI9YV6i7A0fP3MpcJlo2uQVY27RJPcN2NeLfFzXdHr6f5bm9exqB6X1iKimfA2Q==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-6.0.1.tgz", + "integrity": "sha512-z0vvvmwfdozkY1AFqbNLeb/jAWyVwWJOIllZEEwPDKcVtCMPQZ1DRApPMRDRndRL6fOG4aXXnt7C5kgniC+qGw==", "dev": true, "license": "MIT", "dependencies": { "@graphql-codegen/plugin-helpers": "^6.0.0", - "@graphql-codegen/visitor-plugin-common": "6.0.0", + "@graphql-codegen/visitor-plugin-common": "6.0.1", "auto-bind": "~4.0.0", "change-case-all": "1.0.15", "tslib": "~2.6.0" @@ -1287,15 +1287,15 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/typescript": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-5.0.0.tgz", - "integrity": "sha512-u90SGM6+Rdc3Je1EmVQOrGk5fl7hK1cLR4y5Q1MeUenj0aZFxKno65DCW7RcQpcfebvkPsVGA6y3oS02wPFj6Q==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-5.0.1.tgz", + "integrity": "sha512-GqAl4pxFdWTvW1h+Ume7djrucYwt03wiaS88m4ErG+tHsJaR2ZCtoHOo+B4bh7KIuBKap14/xOZG0qY/ThWAhg==", "dev": true, "license": "MIT", "dependencies": { "@graphql-codegen/plugin-helpers": "^6.0.0", "@graphql-codegen/schema-ast": "^5.0.0", - "@graphql-codegen/visitor-plugin-common": "6.0.0", + "@graphql-codegen/visitor-plugin-common": "6.0.1", "auto-bind": "~4.0.0", "tslib": "~2.6.0" }, @@ -1307,15 +1307,15 @@ } }, "node_modules/@graphql-codegen/typescript-operations": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-5.0.0.tgz", - "integrity": "sha512-mqgp/lp5v7w+RYj5AJ/BVquP+sgje3EAgg++62ciolOB5zzWT8en09cRdNq4UZfszCYTOtlhCG7NQAAcSae37A==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-5.0.1.tgz", + "integrity": "sha512-uJwsOIqvXyxlOI1Mnoy8Mn3TiOHTzVTGDwqL9gHnpKqQZdFfvMgfDf/HyT7Mw3XCOfhSS99fe9ATW0bkMExBZg==", "dev": true, "license": "MIT", "dependencies": { "@graphql-codegen/plugin-helpers": "^6.0.0", - "@graphql-codegen/typescript": "^5.0.0", - "@graphql-codegen/visitor-plugin-common": "6.0.0", + "@graphql-codegen/typescript": "^5.0.1", + "@graphql-codegen/visitor-plugin-common": "6.0.1", "auto-bind": "~4.0.0", "tslib": "~2.6.0" }, @@ -1340,15 +1340,15 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/typescript-resolvers": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-resolvers/-/typescript-resolvers-5.0.0.tgz", - "integrity": "sha512-etUYZYwpBM+EmmcH/TtK9+dCzFMM36gI9aIc4/ckDnT34SLWnWVAkbfeNetwzhq98FD84SL5d+YqLGRFeEylJw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-resolvers/-/typescript-resolvers-5.0.1.tgz", + "integrity": "sha512-E3Dyc2gaI4I79Wgwvwo5HP2MMQkUrcGA+3Lfx/ckDlE8zi3wwjWMhAjIhW54VQbi8q2/9h7ooRph3eat9VTscA==", "dev": true, "license": "MIT", "dependencies": { "@graphql-codegen/plugin-helpers": "^6.0.0", - "@graphql-codegen/typescript": "^5.0.0", - "@graphql-codegen/visitor-plugin-common": "6.0.0", + "@graphql-codegen/typescript": "^5.0.1", + "@graphql-codegen/visitor-plugin-common": "6.0.1", "@graphql-tools/utils": "^10.0.0", "auto-bind": "~4.0.0", "tslib": "~2.6.0" @@ -1381,9 +1381,9 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/visitor-plugin-common": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-6.0.0.tgz", - "integrity": "sha512-K05Jv2elOeFstH3i+Ah0Pi9do6NYUvrbdhEkP+UvP9fmIro1hCKwcIEP7j4VFz8mt3gAC3dB5KVJDoyaPUgi4Q==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-6.0.1.tgz", + "integrity": "sha512-3gopoUYXn26PSj2UdCWmYj0QiRVD5qR3eDiXx72OQcN1Vb8qj6VfOWB+NDuD1Q1sgVYbCQVKgj92ERsSW1xH9Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2387,9 +2387,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz", - "integrity": "sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", + "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", "cpu": [ "arm" ], @@ -2401,9 +2401,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.3.tgz", - "integrity": "sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", + "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", "cpu": [ "arm64" ], @@ -2415,9 +2415,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.3.tgz", - "integrity": "sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", + "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", "cpu": [ "arm64" ], @@ -2429,9 +2429,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.3.tgz", - "integrity": "sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", + "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", "cpu": [ "x64" ], @@ -2443,9 +2443,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.3.tgz", - "integrity": "sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", + "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", "cpu": [ "arm64" ], @@ -2457,9 +2457,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.3.tgz", - "integrity": "sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", + "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", "cpu": [ "x64" ], @@ -2471,9 +2471,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.3.tgz", - "integrity": "sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", + "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", "cpu": [ "arm" ], @@ -2485,9 +2485,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.3.tgz", - "integrity": "sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", + "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", "cpu": [ "arm" ], @@ -2499,9 +2499,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.3.tgz", - "integrity": "sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", + "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", "cpu": [ "arm64" ], @@ -2513,9 +2513,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.3.tgz", - "integrity": "sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", + "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", "cpu": [ "arm64" ], @@ -2527,9 +2527,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.3.tgz", - "integrity": "sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", + "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", "cpu": [ "loong64" ], @@ -2541,9 +2541,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.3.tgz", - "integrity": "sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", + "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", "cpu": [ "ppc64" ], @@ -2555,9 +2555,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.3.tgz", - "integrity": "sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", + "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", "cpu": [ "riscv64" ], @@ -2569,9 +2569,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.3.tgz", - "integrity": "sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", + "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", "cpu": [ "riscv64" ], @@ -2583,9 +2583,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.3.tgz", - "integrity": "sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", + "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", "cpu": [ "s390x" ], @@ -2597,9 +2597,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.3.tgz", - "integrity": "sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", + "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", "cpu": [ "x64" ], @@ -2611,9 +2611,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.3.tgz", - "integrity": "sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", + "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", "cpu": [ "x64" ], @@ -2625,9 +2625,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.3.tgz", - "integrity": "sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", + "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", "cpu": [ "arm64" ], @@ -2639,9 +2639,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.3.tgz", - "integrity": "sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", + "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", "cpu": [ "arm64" ], @@ -2653,9 +2653,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.3.tgz", - "integrity": "sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", + "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", "cpu": [ "ia32" ], @@ -2667,9 +2667,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.3.tgz", - "integrity": "sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", + "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", "cpu": [ "x64" ], @@ -2681,9 +2681,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.3.tgz", - "integrity": "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", + "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", "cpu": [ "x64" ], @@ -2794,13 +2794,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", - "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "version": "24.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.2.tgz", + "integrity": "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.12.0" + "undici-types": "~7.13.0" } }, "node_modules/@types/prismjs": { @@ -3030,9 +3030,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.8.tgz", - "integrity": "sha512-be0PUaPsQX/gPWWgFsdD+GFzaoig5PXaUC1xLkQiYdDnANU8sMnHoQd8JhbJQuvTWrWLyeFN9Imb5Qtfvr4RrQ==", + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.10.tgz", + "integrity": "sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3063,9 +3063,9 @@ } }, "node_modules/browserslist": { - "version": "4.26.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", - "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", "dev": true, "funding": [ { @@ -3083,9 +3083,9 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.3", - "caniuse-lite": "^1.0.30001741", - "electron-to-chromium": "^1.5.218", + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, @@ -3135,9 +3135,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001745", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", - "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==", + "version": "1.0.30001747", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001747.tgz", + "integrity": "sha512-mzFa2DGIhuc5490Nd/G31xN1pnBnYMadtkyTjefPI7wzypqgCEpeWu9bJr0OnDsyKrW75zA9ZAt7pbQFmwLsQg==", "dev": true, "funding": [ { @@ -3588,9 +3588,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.227", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.227.tgz", - "integrity": "sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA==", + "version": "1.5.230", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.230.tgz", + "integrity": "sha512-A6A6Fd3+gMdaed9wX83CvHYJb4UuapPD5X5SLq72VZJzxHSY0/LUweGXRWmQlh2ln7KV7iw7jnwXK7dlPoOnHQ==", "dev": true, "license": "ISC" }, @@ -4292,9 +4292,9 @@ } }, "node_modules/jiti": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz", - "integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", "bin": { @@ -4369,9 +4369,9 @@ } }, "node_modules/lightningcss": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", - "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -4385,22 +4385,44 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-darwin-arm64": "1.30.1", - "lightningcss-darwin-x64": "1.30.1", - "lightningcss-freebsd-x64": "1.30.1", - "lightningcss-linux-arm-gnueabihf": "1.30.1", - "lightningcss-linux-arm64-gnu": "1.30.1", - "lightningcss-linux-arm64-musl": "1.30.1", - "lightningcss-linux-x64-gnu": "1.30.1", - "lightningcss-linux-x64-musl": "1.30.1", - "lightningcss-win32-arm64-msvc": "1.30.1", - "lightningcss-win32-x64-msvc": "1.30.1" + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", - "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", "cpu": [ "arm64" ], @@ -4419,9 +4441,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", - "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", "cpu": [ "x64" ], @@ -4440,9 +4462,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", - "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", "cpu": [ "x64" ], @@ -4461,9 +4483,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", - "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", "cpu": [ "arm" ], @@ -4482,9 +4504,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", - "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", "cpu": [ "arm64" ], @@ -4503,9 +4525,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", - "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", "cpu": [ "arm64" ], @@ -4524,9 +4546,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", - "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", "cpu": [ "x64" ], @@ -4545,9 +4567,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", - "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", "cpu": [ "x64" ], @@ -4566,9 +4588,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", - "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", "cpu": [ "arm64" ], @@ -4587,9 +4609,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", - "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", "cpu": [ "x64" ], @@ -5408,9 +5430,9 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.3.tgz", - "integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", + "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5424,28 +5446,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.3", - "@rollup/rollup-android-arm64": "4.52.3", - "@rollup/rollup-darwin-arm64": "4.52.3", - "@rollup/rollup-darwin-x64": "4.52.3", - "@rollup/rollup-freebsd-arm64": "4.52.3", - "@rollup/rollup-freebsd-x64": "4.52.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.3", - "@rollup/rollup-linux-arm-musleabihf": "4.52.3", - "@rollup/rollup-linux-arm64-gnu": "4.52.3", - "@rollup/rollup-linux-arm64-musl": "4.52.3", - "@rollup/rollup-linux-loong64-gnu": "4.52.3", - "@rollup/rollup-linux-ppc64-gnu": "4.52.3", - "@rollup/rollup-linux-riscv64-gnu": "4.52.3", - "@rollup/rollup-linux-riscv64-musl": "4.52.3", - "@rollup/rollup-linux-s390x-gnu": "4.52.3", - "@rollup/rollup-linux-x64-gnu": "4.52.3", - "@rollup/rollup-linux-x64-musl": "4.52.3", - "@rollup/rollup-openharmony-arm64": "4.52.3", - "@rollup/rollup-win32-arm64-msvc": "4.52.3", - "@rollup/rollup-win32-ia32-msvc": "4.52.3", - "@rollup/rollup-win32-x64-gnu": "4.52.3", - "@rollup/rollup-win32-x64-msvc": "4.52.3", + "@rollup/rollup-android-arm-eabi": "4.52.4", + "@rollup/rollup-android-arm64": "4.52.4", + "@rollup/rollup-darwin-arm64": "4.52.4", + "@rollup/rollup-darwin-x64": "4.52.4", + "@rollup/rollup-freebsd-arm64": "4.52.4", + "@rollup/rollup-freebsd-x64": "4.52.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", + "@rollup/rollup-linux-arm-musleabihf": "4.52.4", + "@rollup/rollup-linux-arm64-gnu": "4.52.4", + "@rollup/rollup-linux-arm64-musl": "4.52.4", + "@rollup/rollup-linux-loong64-gnu": "4.52.4", + "@rollup/rollup-linux-ppc64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-musl": "4.52.4", + "@rollup/rollup-linux-s390x-gnu": "4.52.4", + "@rollup/rollup-linux-x64-gnu": "4.52.4", + "@rollup/rollup-linux-x64-musl": "4.52.4", + "@rollup/rollup-openharmony-arm64": "4.52.4", + "@rollup/rollup-win32-arm64-msvc": "4.52.4", + "@rollup/rollup-win32-ia32-msvc": "4.52.4", + "@rollup/rollup-win32-x64-gnu": "4.52.4", + "@rollup/rollup-win32-x64-msvc": "4.52.4", "fsevents": "~2.3.2" } }, @@ -5903,9 +5925,9 @@ "license": "0BSD" }, "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5954,9 +5976,9 @@ } }, "node_modules/undici-types": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", - "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz", + "integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==", "dev": true, "license": "MIT" }, @@ -6039,9 +6061,9 @@ "license": "ISC" }, "node_modules/vite": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz", - "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==", + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz", + "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==", "dev": true, "license": "MIT", "dependencies": { @@ -6114,9 +6136,9 @@ } }, "node_modules/vite-plugin-solid": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.11.8.tgz", - "integrity": "sha512-hFrCxBfv3B1BmFqnJF4JOCYpjrmi/zwyeKjcomQ0khh8HFyQ8SbuBWQ7zGojfrz6HUOBFrJBNySDi/JgAHytWg==", + "version": "2.11.9", + "resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.11.9.tgz", + "integrity": "sha512-bTA6p+bspXZsuulSd2y6aTzegF8xGaJYcq1Uyh/mv+W4DQtzCgL9nN6n2fsTaxp/dMk+ZHHKgGndlNeooqHLKw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 2d2be029..8ace47f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "publy-panel", - "version": "0.9.28", + "version": "0.9.30", "type": "module", "description": "Publy, a modern platform for collaborative text creation, offers a user-friendly interface for authors, editors, and readers, supporting real-time collaboration and structured feedback.", "scripts": { @@ -13,27 +13,27 @@ "codegen": "graphql-codegen --config codegen.ts" }, "devDependencies": { - "@biomejs/biome": "^2.2.4", + "@biomejs/biome": "^2.2.5", "@graphql-codegen/cli": "^6.0.0", - "@graphql-codegen/client-preset": "^5.0.1", + "@graphql-codegen/client-preset": "^5.0.2", "@graphql-codegen/introspection": "^5.0.0", - "@graphql-codegen/typescript": "^5.0.0", - "@graphql-codegen/typescript-operations": "^5.0.0", - "@graphql-codegen/typescript-resolvers": "^5.0.0", + "@graphql-codegen/typescript": "^5.0.1", + "@graphql-codegen/typescript-operations": "^5.0.1", + "@graphql-codegen/typescript-resolvers": "^5.0.1", "@solidjs/router": "^0.15.3", - "@types/node": "^24.5.2", + "@types/node": "^24.6.2", "@types/prismjs": "^1.26.5", "graphql": "^16.11.0", "graphql-tag": "^2.12.6", - "lightningcss": "^1.30.1", + "lightningcss": "^1.30.2", "prismjs": "^1.30.0", "solid-js": "^1.9.9", "terser": "^5.44.0", - "typescript": "^5.9.2", - "vite": "^7.1.7", - "vite-plugin-solid": "^2.11.7" + "typescript": "^5.9.3", + "vite": "^7.1.9", + "vite-plugin-solid": "^2.11.9" }, "overrides": { - "vite": "^7.1.7" + "vite": "^7.1.9" } } diff --git a/panel/context/auth.tsx b/panel/context/auth.tsx index e88ff707..3f2d5b08 100644 --- a/panel/context/auth.tsx +++ b/panel/context/auth.tsx @@ -69,7 +69,7 @@ export const AuthProvider: Component = (props) => { // НачинаСм с false Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΈΠ·Π±Π΅ΠΆΠ°Ρ‚ΡŒ мСрцания, Ρ€Π΅Π°Π»ΡŒΠ½Π°Ρ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π±ΡƒΠ΄Π΅Ρ‚ Π² onMount const [isAuthenticated, setIsAuthenticated] = createSignal(false) const [isReady, setIsReady] = createSignal(false) - + // Π€Π»Π°Π³ для прСдотвращСния ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½Ρ‹Ρ… ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΠΉ let isInitializing = false @@ -82,7 +82,7 @@ export const AuthProvider: Component = (props) => { console.log('[AuthProvider] Already initializing, skipping...') return } - + isInitializing = true console.log('[AuthProvider] Performing auth initialization...') @@ -91,7 +91,7 @@ export const AuthProvider: Component = (props) => { console.log('[AuthProvider] Checking authentication via GraphQL...') // ДобавляСм Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚ для запроса (5 сСкунд для Π»ΡƒΡ‡ΡˆΠ΅Π³ΠΎ UX) - const timeoutPromise = new Promise((_, reject) => + const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Auth check timeout')), 5000) ) @@ -159,10 +159,10 @@ export const AuthProvider: Component = (props) => { const logout = async () => { console.log('[AuthProvider] Attempting logout...') - + // ΠŸΡ€Π΅Π΄ΠΎΡ‚Π²Ρ€Π°Ρ‰Π°Π΅ΠΌ ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½Ρ‹Π΅ ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ Π²ΠΎ врСмя logout isInitializing = true - + try { // Π‘Π½Π°Ρ‡Π°Π»Π° ΠΎΡ‡ΠΈΡ‰Π°Π΅ΠΌ Ρ‚ΠΎΠΊΠ΅Π½Ρ‹ Π½Π° ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π΅ clearAuthTokens() diff --git a/panel/routes/env.tsx b/panel/routes/env.tsx index f02dae98..1e569f91 100644 --- a/panel/routes/env.tsx +++ b/panel/routes/env.tsx @@ -1,6 +1,6 @@ import { Component, createSignal, For, Show } from 'solid-js' import { query } from '../graphql' -import type { EnvSection, EnvVariable, Query } from '../graphql/generated/schema' +import type { EnvSection, EnvVariable, Query } from '../graphql/generated/graphql' import { ADMIN_UPDATE_ENV_VARIABLE_MUTATION } from '../graphql/mutations' import { ADMIN_GET_ENV_VARIABLES_QUERY } from '../graphql/queries' import EnvVariableModal from '../modals/EnvVariableModal' diff --git a/pyproject.toml b/pyproject.toml index a576d879..fb7e7b1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "discours-core" -version = "0.9.28" +version = "0.9.32" description = "Core backend for Discours.io platform" authors = [ {name = "Tony Rewin", email = "tonyrewin@yandex.ru"} diff --git a/resolvers/auth.py b/resolvers/auth.py index 4e813556..3d334517 100644 --- a/resolvers/auth.py +++ b/resolvers/auth.py @@ -27,11 +27,15 @@ from utils.logger import root_logger as logger def resolve_roles(obj: dict | Any, info: GraphQLResolveInfo) -> list[str]: """Π Π΅Π·ΠΎΠ»Π²Π΅Ρ€ для поля roles Π°Π²Ρ‚ΠΎΡ€Π°""" try: + # Если это ORM ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ с ΠΌΠ΅Ρ‚ΠΎΠ΄ΠΎΠΌ get_roles if hasattr(obj, "get_roles"): return obj.get_roles() + # Если это ΡΠ»ΠΎΠ²Π°Ρ€ΡŒ if isinstance(obj, dict): - roles_data = obj.get("roles_data", {}) + roles_data = obj.get("roles_data") + if roles_data is None: + return [] if isinstance(roles_data, list): return roles_data if isinstance(roles_data, dict): @@ -122,9 +126,12 @@ async def login(_: None, info: GraphQLResolveInfo, **kwargs: Any) -> dict[str, A domain=SESSION_COOKIE_DOMAIN, ) - logger.info( - f"βœ… Admin login: httpOnly cookie установлСн для ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ {result.get('author', {}).get('id')}" + author_id = ( + result.get("author", {}).get("id") + if isinstance(result.get("author"), dict) + else getattr(result.get("author"), "id", "unknown") ) + logger.info(f"βœ… Admin login: httpOnly cookie установлСн для ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ {author_id}") # Для Π°Π΄ΠΌΠΈΠ½ΠΊΠΈ НЕ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌ Ρ‚ΠΎΠΊΠ΅Π½ ΠΊΠ»ΠΈΠ΅Π½Ρ‚Ρƒ - ΠΎΠ½ Π² httpOnly cookie result_without_token = result.copy() @@ -136,9 +143,12 @@ async def login(_: None, info: GraphQLResolveInfo, **kwargs: Any) -> dict[str, A # Для основного сайта Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌ Ρ‚ΠΎΠΊΠ΅Π½ ΠΊΠ°ΠΊ ΠΎΠ±Ρ‹Ρ‡Π½ΠΎ (Bearer Π² localStorage) if not is_admin_request: - logger.info( - f"βœ… Main site login: Ρ‚ΠΎΠΊΠ΅Π½ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π΅Π½ для localStorage ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ {result.get('author', {}).get('id')}" + author_id = ( + result.get("author", {}).get("id") + if isinstance(result.get("author"), dict) + else getattr(result.get("author"), "id", "unknown") ) + logger.info(f"βœ… Main site login: Ρ‚ΠΎΠΊΠ΅Π½ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π΅Π½ для localStorage ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ {author_id}") return result except Exception as e: diff --git a/resolvers/author.py b/resolvers/author.py index 062abf56..a03f3c47 100644 --- a/resolvers/author.py +++ b/resolvers/author.py @@ -18,7 +18,7 @@ from cache.cache import ( from orm.author import Author, AuthorFollower from orm.community import Community, CommunityAuthor, CommunityFollower from orm.reaction import Reaction -from orm.shout import Shout, ShoutAuthor, ShoutTopic +from orm.shout import Shout, ShoutAuthor, ShoutReactionsFollower, ShoutTopic from orm.topic import Topic from resolvers.stat import get_with_stat from services.auth import login_required @@ -974,12 +974,23 @@ async def get_author_follows( has_access = is_admin or (viewer_id is not None and str(viewer_id) == str(temp_author.id)) followed_authors.append(temp_author.dict(has_access)) + # ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ подписанныС ΡˆΠ°ΡƒΡ‚Ρ‹ + followed_shouts = [] + with local_session() as session: + shout_followers = ( + session.query(ShoutReactionsFollower).filter(ShoutReactionsFollower.follower == author_id).all() + ) + for sf in shout_followers: + shout = session.query(Shout).filter(Shout.id == sf.shout).first() + if shout: + followed_shouts.append(shout.dict()) + followed_communities = DEFAULT_COMMUNITIES # TODO: get followed communities return { "authors": followed_authors, "topics": followed_topics, "communities": followed_communities, - "shouts": [], + "shouts": followed_shouts, "error": None, } diff --git a/resolvers/draft.py b/resolvers/draft.py index 524906d4..da808ee0 100644 --- a/resolvers/draft.py +++ b/resolvers/draft.py @@ -274,6 +274,108 @@ async def create_draft(_: None, info: GraphQLResolveInfo, draft_input: dict[str, return {"error": f"Failed to create draft: {e!s}"} +@mutation.field("create_draft_from_shout") +@login_required +async def create_draft_from_shout(_: None, info: GraphQLResolveInfo, shout_id: int) -> dict[str, Any]: + """ + Π‘ΠΎΠ·Π΄Π°Ρ‘Ρ‚ Ρ‡Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊ ΠΈΠ· ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰Π΅Π³ΠΎ ΠΎΠΏΡƒΠ±Π»ΠΈΠΊΠΎΠ²Π°Π½Π½ΠΎΠ³ΠΎ ΡˆΠ°ΡƒΡ‚Π° для рСдактирования. + + Args: + info: GraphQL context + shout_id (int): ID ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ (shout) + + Returns: + dict: Contains either: + - draft: The created draft object with shout reference + - error: Error message if creation failed + + Example: + >>> async def test_create_from_shout(): + ... context = {'user_id': '123', 'author': {'id': 1}} + ... info = type('Info', (), {'context': context})() + ... result = await create_draft_from_shout(None, info, 42) + ... assert result.get('error') is None + ... assert result['draft'].shout == 42 + ... return result + """ + author_dict = info.context.get("author") or {} + author_id = author_dict.get("id") + + if not author_id or not isinstance(author_id, int): + return {"error": "Author ID is required"} + + try: + with local_session() as session: + # Π—Π°Π³Ρ€ΡƒΠΆΠ°Π΅ΠΌ ΡˆΠ°ΡƒΡ‚ с Π°Π²Ρ‚ΠΎΡ€Π°ΠΌΠΈ ΠΈ Ρ‚Π΅ΠΌΠ°ΠΌΠΈ + shout = ( + session.query(Shout) + .options(joinedload(Shout.authors), joinedload(Shout.topics)) + .where(Shout.id == shout_id) + .first() + ) + + if not shout: + return {"error": f"Shout with id={shout_id} not found"} + + # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, Ρ‡Ρ‚ΠΎ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ являСтся Π°Π²Ρ‚ΠΎΡ€ΠΎΠΌ ΡˆΠ°ΡƒΡ‚Π° + author_ids = [a.id for a in shout.authors] + if author_id not in author_ids: + return {"error": "You are not authorized to edit this shout"} + + # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, Π½Π΅Ρ‚ Π»ΠΈ ΡƒΠΆΠ΅ Ρ‡Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊΠ° для этого ΡˆΠ°ΡƒΡ‚Π° + existing_draft = session.query(Draft).where(Draft.shout == shout_id).first() + if existing_draft: + logger.info(f"Draft already exists for shout {shout_id}: draft_id={existing_draft.id}") + return {"draft": create_draft_dict(existing_draft)} + + # Π‘ΠΎΠ·Π΄Π°Ρ‘ΠΌ Π½ΠΎΠ²Ρ‹ΠΉ Ρ‡Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊ ΠΈΠ· ΡˆΠ°ΡƒΡ‚Π° + now = int(time.time()) + draft = Draft( + created_at=now, + created_by=author_id, + community=shout.community, + layout=shout.layout or "article", + title=shout.title or "", + subtitle=shout.subtitle, + body=shout.body or "", + lead=shout.lead, + slug=shout.slug, + cover=shout.cover, + cover_caption=shout.cover_caption, + seo=shout.seo, + media=shout.media, + lang=shout.lang or "ru", + shout=shout_id, # БвязываСм с ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠΌ ΡˆΠ°ΡƒΡ‚ΠΎΠΌ + ) + + session.add(draft) + session.flush() + + # ΠšΠΎΠΏΠΈΡ€ΡƒΠ΅ΠΌ Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² ΠΈΠ· ΡˆΠ°ΡƒΡ‚Π° + for author in shout.authors: + da = DraftAuthor(draft=draft.id, author=author.id) + session.add(da) + + # ΠšΠΎΠΏΠΈΡ€ΡƒΠ΅ΠΌ Ρ‚Π΅ΠΌΡ‹ ΠΈΠ· ΡˆΠ°ΡƒΡ‚Π° + shout_topics = session.query(ShoutTopic).where(ShoutTopic.shout == shout_id).all() + for st in shout_topics: + dt = DraftTopic(draft=draft.id, topic=st.topic, main=st.main) + session.add(dt) + + session.commit() + + logger.info(f"Created draft {draft.id} from shout {shout_id}") + + # Π€ΠΎΡ€ΠΌΠΈΡ€ΡƒΠ΅ΠΌ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ + draft_dict = create_draft_dict(draft) + + return {"draft": draft_dict} + + except Exception as e: + logger.error(f"Failed to create draft from shout {shout_id}: {e}", exc_info=True) + return {"error": f"Failed to create draft from shout: {e!s}"} + + def generate_teaser(body: str, limit: int = 300) -> str: body_text = extract_text(body) return ". ".join(body_text[:limit].split(". ")[:-1]) diff --git a/resolvers/follower.py b/resolvers/follower.py index 86df0c86..9cafb693 100644 --- a/resolvers/follower.py +++ b/resolvers/follower.py @@ -25,8 +25,36 @@ from utils.logger import root_logger as logger def get_entity_field_name(entity_type: str) -> str: - """Π’ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ имя поля для связи с ΡΡƒΡ‰Π½ΠΎΡΡ‚ΡŒΡŽ Π² ΠΌΠΎΠ΄Π΅Π»ΠΈ подписчика""" - entity_field_mapping = {"author": "following", "topic": "topic", "community": "community", "shout": "shout"} + """ + Π’ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ имя поля для связи с ΡΡƒΡ‰Π½ΠΎΡΡ‚ΡŒΡŽ Π² ΠΌΠΎΠ΄Π΅Π»ΠΈ подписчика. + + Π­Ρ‚Π° функция ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ для опрСдСлСния ΠΏΡ€Π°Π²ΠΈΠ»ΡŒΠ½ΠΎΠ³ΠΎ поля Π² модСлях подписчиков + (AuthorFollower, TopicFollower, CommunityFollower, ShoutReactionsFollower) ΠΏΡ€ΠΈ создании + ΠΈΠ»ΠΈ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ΅ подписки. + + Args: + entity_type: Π’ΠΈΠΏ сущности Π² Π½ΠΈΠΆΠ½Π΅ΠΌ рСгистрС ('author', 'topic', 'community', 'shout') + + Returns: + str: Имя поля Π² ΠΌΠΎΠ΄Π΅Π»ΠΈ подписчика ('following', 'topic', 'community', 'shout') + + Raises: + ValueError: Если ΠΏΠ΅Ρ€Π΅Π΄Π°Π½ нСизвСстный Ρ‚ΠΈΠΏ сущности + + Examples: + >>> get_entity_field_name('author') + 'following' + >>> get_entity_field_name('topic') + 'topic' + >>> get_entity_field_name('invalid') + ValueError: Unknown entity_type: invalid + """ + entity_field_mapping = { + "author": "following", # AuthorFollower.following -> Author + "topic": "topic", # TopicFollower.topic -> Topic + "community": "community", # CommunityFollower.community -> Community + "shout": "shout", # ShoutReactionsFollower.shout -> Shout + } if entity_type not in entity_field_mapping: msg = f"Unknown entity_type: {entity_type}" raise ValueError(msg) @@ -38,11 +66,54 @@ def get_entity_field_name(entity_type: str) -> str: async def follow( _: None, info: GraphQLResolveInfo, what: str, slug: str = "", entity_id: int | None = None ) -> dict[str, Any]: + """ + GraphQL мутация для создания подписки Π½Π° Π°Π²Ρ‚ΠΎΡ€Π°, Ρ‚Π΅ΠΌΡƒ, сообщСство ΠΈΠ»ΠΈ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΡŽ. + + Π­Ρ‚Π° функция ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°Π΅Ρ‚ всС Ρ‚ΠΈΠΏΡ‹ подписок Π² систСмС, Π²ΠΊΠ»ΡŽΡ‡Π°Ρ: + - ΠŸΠΎΠ΄ΠΏΠΈΡΠΊΡƒ Π½Π° Π°Π²Ρ‚ΠΎΡ€Π° (AUTHOR) + - ΠŸΠΎΠ΄ΠΏΠΈΡΠΊΡƒ Π½Π° Ρ‚Π΅ΠΌΡƒ (TOPIC) + - ΠŸΠΎΠ΄ΠΏΠΈΡΠΊΡƒ Π½Π° сообщСство (COMMUNITY) + - ΠŸΠΎΠ΄ΠΏΠΈΡΠΊΡƒ Π½Π° ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΡŽ (SHOUT) + + Args: + _: None - Π‘Ρ‚Π°Π½Π΄Π°Ρ€Ρ‚Π½Ρ‹ΠΉ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ GraphQL (Π½Π΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ) + info: GraphQLResolveInfo - ΠšΠΎΠ½Ρ‚Π΅ΠΊΡΡ‚ GraphQL запроса, содСрТит ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ ΠΎΠ± Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·ΠΎΠ²Π°Π½Π½ΠΎΠΌ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ + what: str - Π’ΠΈΠΏ сущности для подписки ('AUTHOR', 'TOPIC', 'COMMUNITY', 'SHOUT') + slug: str - Slug сущности (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, 'author-slug' ΠΈΠ»ΠΈ 'topic-slug') + entity_id: int | None - ID сущности (Π°Π»ΡŒΡ‚Π΅Ρ€Π½Π°Ρ‚ΠΈΠ²Π° slug) + + Returns: + dict[str, Any] - Π Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ: + { + "success": bool, # Π£ΡΠΏΠ΅ΡˆΠ½ΠΎΡΡ‚ΡŒ ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ + "error": str | None, # ВСкст ошибки Ссли Π΅ΡΡ‚ΡŒ + "authors": Author[], # ΠžΠ±Π½ΠΎΠ²Π»Π΅Π½Π½Ρ‹Π΅ Π°Π²Ρ‚ΠΎΡ€Ρ‹ (для ΠΊΠ΅ΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡ) + "topics": Topic[], # ΠžΠ±Π½ΠΎΠ²Π»Π΅Π½Π½Ρ‹Π΅ Ρ‚Π΅ΠΌΡ‹ (для ΠΊΠ΅ΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡ) + "entity_id": int | None # ID созданной подписки + } + + Raises: + ValueError: ΠŸΡ€ΠΈ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‡Π΅ Π½Π΅ΠΊΠΎΡ€Ρ€Π΅ΠΊΡ‚Π½Ρ‹Ρ… ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ΠΎΠ² + DatabaseError: ΠŸΡ€ΠΈ ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΠ°Ρ… с Π±Π°Π·ΠΎΠΉ Π΄Π°Π½Π½Ρ‹Ρ… + """ logger.debug("Начало выполнСния Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ 'follow'") viewer_id = info.context.get("author", {}).get("id") - if not viewer_id: - return {"error": "Access denied"} follower_dict = info.context.get("author") or {} + + # βœ… КРИВИЧНО: Π˜Π½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ кСш Π’ БАМОМ НАЧАЛЕ, Ссли ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·ΠΎΠ²Π°Π½ + # Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΏΡ€Π΅Π΄ΠΎΡ‚Π²Ρ€Π°Ρ‚ΠΈΡ‚ΡŒ Ρ‡Ρ‚Π΅Π½ΠΈΠ΅ старых Π΄Π°Π½Π½Ρ‹Ρ… ΠΏΡ€ΠΈ ΠΏΠΎΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅ΠΉ ΠΏΠ΅Ρ€Π΅Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ΅ + if viewer_id: + entity_type = what.lower() + cache_key_pattern = f"author:follows-{entity_type}s:{viewer_id}" + await redis.execute("DEL", cache_key_pattern) + await redis.execute("DEL", f"author:id:{viewer_id}") + logger.debug(f"Π˜Π½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΠΎΠ²Π°Π½ кСш подписок follower'Π°: {cache_key_pattern}") + + # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·Π°Ρ†ΠΈΠΈ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ + if not viewer_id: + logger.warning("ΠŸΠΎΠΏΡ‹Ρ‚ΠΊΠ° ΠΏΠΎΠ΄ΠΏΠΈΡΠ°Ρ‚ΡŒΡΡ Π±Π΅Π· Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·Π°Ρ†ΠΈΠΈ") + return {"error": "Access denied"} + logger.debug(f"follower: {follower_dict}") if not viewer_id or not follower_dict: @@ -52,6 +123,7 @@ async def follow( follower_id = follower_dict.get("id") logger.debug(f"follower_id: {follower_id}") + # Маппинг Ρ‚ΠΈΠΏΠΎΠ² сущностСй Π½Π° ΠΈΡ… классы ΠΈ ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹ ΠΊΠ΅ΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡ entity_classes = { "AUTHOR": (Author, AuthorFollower, get_cached_follower_authors, cache_author), "TOPIC": (Topic, TopicFollower, get_cached_follower_topics, cache_topic), @@ -68,6 +140,10 @@ async def follow( follows: list[dict[str, Any]] = [] error: str | None = None + # βœ… БохраняСм entity_id ΠΈ error Π²Π½Π΅ сСссии для использования послС Π΅Ρ‘ закрытия + entity_id_result: int | None = None + error_result: str | None = None + try: logger.debug("ΠŸΠΎΠΏΡ‹Ρ‚ΠΊΠ° ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ ΡΡƒΡ‰Π½ΠΎΡΡ‚ΡŒ ΠΈΠ· Π±Π°Π·Ρ‹ Π΄Π°Π½Π½Ρ‹Ρ…") with local_session() as session: @@ -109,9 +185,11 @@ async def follow( ) .first() ) + if existing_sub: logger.info(f"ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ {follower_id} ΡƒΠΆΠ΅ подписан Π½Π° {what.lower()} с ID {entity_id}") - error = "already following" + error_result = "already following" + # βœ… КРИВИЧНО: НС Π΄Π΅Π»Π°Π΅ΠΌ return - ΠΏΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠ°Π΅ΠΌ для получСния списка подписок else: logger.debug("Π”ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ Π½ΠΎΠ²ΠΎΠΉ записи Π² Π±Π°Π·Ρƒ Π΄Π°Π½Π½Ρ‹Ρ…") sub = follower_class(follower=follower_id, **{entity_field: entity_id}) @@ -120,42 +198,41 @@ async def follow( session.commit() logger.info(f"ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ {follower_id} подписался Π½Π° {what.lower()} с ID {entity_id}") - # Π˜Π½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ кэш подписок ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ послС любой ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ - cache_key_pattern = f"author:follows-{entity_type}s:{follower_id}" - await redis.execute("DEL", cache_key_pattern) - logger.debug(f"Π˜Π½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΠΎΠ²Π°Π½ кэш подписок: {cache_key_pattern}") + if cache_method: + logger.debug("ОбновлСниС кэша сущности") + await cache_method(entity_dict) - if cache_method: - logger.debug("ОбновлСниС кэша сущности") - await cache_method(entity_dict) + if what == "AUTHOR": + logger.debug("ΠžΡ‚ΠΏΡ€Π°Π²ΠΊΠ° увСдомлСния Π°Π²Ρ‚ΠΎΡ€Ρƒ ΠΎ подпискС") + if isinstance(follower_dict, dict) and isinstance(entity_id, int): + # ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ID созданной записи подписки + subscription_id = getattr(sub, "id", None) if "sub" in locals() else None + await notify_follower( + follower=follower_dict, + author_id=entity_id, + action="follow", + subscription_id=subscription_id, + ) - # Π˜Π½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ кэш подписок ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ для обновлСния списка подписок - cache_key_pattern = f"author:follows-{entity_type}s:{follower_id}" - await redis.execute("DEL", cache_key_pattern) - logger.debug(f"Π˜Π½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΠΎΠ²Π°Π½ кэш подписок: {cache_key_pattern}") + # βœ… КРИВИЧНО: Π˜Π½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ кСш списка подписчиков Π°Π²Ρ‚ΠΎΡ€Π° + # Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π½ΠΎΠ²Ρ‹ΠΉ подписчик сразу появился Π² спискС + await redis.execute("DEL", f"author:followers:{entity_id}") + logger.debug(f"Π˜Π½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΠΎΠ²Π°Π½ кСш подписчиков Π°Π²Ρ‚ΠΎΡ€Π°: author:followers:{entity_id}") - if what == "AUTHOR" and not existing_sub: - logger.debug("ΠžΡ‚ΠΏΡ€Π°Π²ΠΊΠ° увСдомлСния Π°Π²Ρ‚ΠΎΡ€Ρƒ ΠΎ подпискС") - if isinstance(follower_dict, dict) and isinstance(entity_id, int): - # ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ID созданной записи подписки - subscription_id = getattr(sub, "id", None) if "sub" in locals() else None - await notify_follower( - follower=follower_dict, - author_id=entity_id, - action="follow", - subscription_id=subscription_id, - ) + # Π˜Π½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ кСш статистики Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² для обновлСния счСтчиков подписчиков + logger.debug("Π˜Π½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ кСш статистики Π°Π²Ρ‚ΠΎΡ€ΠΎΠ²") + await invalidate_authors_cache(entity_id) - # Π˜Π½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ кСш статистики Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² для обновлСния счСтчиков подписчиков - logger.debug("Π˜Π½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ кСш статистики Π°Π²Ρ‚ΠΎΡ€ΠΎΠ²") - await invalidate_authors_cache(entity_id) + entity_id_result = entity_id - # ВсСгда ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ Π°ΠΊΡ‚ΡƒΠ°Π»ΡŒΠ½Ρ‹ΠΉ список подписок для Π²ΠΎΠ·Π²Ρ€Π°Ρ‚Π° ΠΊΠ»ΠΈΠ΅Π½Ρ‚Ρƒ + # βœ… ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ Π°ΠΊΡ‚ΡƒΠ°Π»ΡŒΠ½Ρ‹ΠΉ список подписок для Π²ΠΎΠ·Π²Ρ€Π°Ρ‚Π° ΠΊΠ»ΠΈΠ΅Π½Ρ‚Ρƒ + # КСш ΡƒΠΆΠ΅ ΠΈΠ½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΠΎΠ²Π°Π½ Π² Π½Π°Ρ‡Π°Π»Π΅ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ, поэтому get_cached_follows_method + # Π²Π΅Ρ€Π½Π΅Ρ‚ свСТиС Π΄Π°Π½Π½Ρ‹Π΅ ΠΈΠ· Π‘Π” if get_cached_follows_method and isinstance(follower_id, int): - logger.debug("ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ Π°ΠΊΡ‚ΡƒΠ°Π»ΡŒΠ½ΠΎΠ³ΠΎ списка подписок ΠΈΠ· кэша") + logger.debug("ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ Π°ΠΊΡ‚ΡƒΠ°Π»ΡŒΠ½ΠΎΠ³ΠΎ списка подписок послС закрытия сСссии") existing_follows = await get_cached_follows_method(follower_id) logger.debug( - f"ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΎ подписок: {len(existing_follows)}, содСрТит target={entity_id in [f.get('id') for f in existing_follows] if existing_follows else False}" + f"ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΎ подписок: {len(existing_follows)}, содСрТит target={entity_id_result in [f.get('id') for f in existing_follows] if existing_follows else False}" ) # Если это Π°Π²Ρ‚ΠΎΡ€Ρ‹, ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ Π±Π΅Π·ΠΎΠΏΠ°ΡΠ½ΡƒΡŽ Π²Π΅Ρ€ΡΠΈΡŽ @@ -179,7 +256,7 @@ async def follow( logger.debug(f"ΠΠΊΡ‚ΡƒΠ°Π»ΡŒΠ½Ρ‹ΠΉ список подписок ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½: {len(follows)} элСмСнтов") - return {f"{entity_type}s": follows, "error": error} + return {f"{entity_type}s": follows, "error": error_result} except Exception as exc: logger.exception("ΠŸΡ€ΠΎΠΈΠ·ΠΎΡˆΠ»Π° ошибка Π² Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ 'follow'") @@ -191,11 +268,93 @@ async def follow( async def unfollow( _: None, info: GraphQLResolveInfo, what: str, slug: str = "", entity_id: int | None = None ) -> dict[str, Any]: + """ + GraphQL мутация для ΠΎΡ‚ΠΌΠ΅Π½Ρ‹ подписки Π½Π° Π°Π²Ρ‚ΠΎΡ€Π°, Ρ‚Π΅ΠΌΡƒ, сообщСство ΠΈΠ»ΠΈ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΡŽ. + + Π­Ρ‚Π° функция ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°Π΅Ρ‚ ΠΎΡ‚ΠΌΠ΅Π½Ρƒ всСх Ρ‚ΠΈΠΏΠΎΠ² подписок Π² систСмС, Π²ΠΊΠ»ΡŽΡ‡Π°Ρ: + - ΠžΡ‚ΠΏΠΈΡΠΊΡƒ ΠΎΡ‚ Π°Π²Ρ‚ΠΎΡ€Π° (AUTHOR) + - ΠžΡ‚ΠΏΠΈΡΠΊΡƒ ΠΎΡ‚ Ρ‚Π΅ΠΌΡ‹ (TOPIC) + - ΠžΡ‚ΠΏΠΈΡΠΊΡƒ ΠΎΡ‚ сообщСства (COMMUNITY) + - ΠžΡ‚ΠΏΠΈΡΠΊΡƒ ΠΎΡ‚ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ (SHOUT) + + ΠŸΡ€ΠΎΡ†Π΅ΡΡ ΠΎΡ‚ΠΌΠ΅Π½Ρ‹ подписки: + 1. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·Π°Ρ†ΠΈΠΈ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ + 2. Поиск ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰Π΅ΠΉ подписки Π² Π±Π°Π·Π΅ Π΄Π°Π½Π½Ρ‹Ρ… + 3. Π£Π΄Π°Π»Π΅Π½ΠΈΠ΅ подписки Ссли ΠΎΠ½Π° Π½Π°ΠΉΠ΄Π΅Π½Π° + 4. Π˜Π½Π²Π°Π»ΠΈΠ΄Π°Ρ†ΠΈΡ кСша для обновлСния Π΄Π°Π½Π½Ρ‹Ρ… + 5. ΠžΡ‚ΠΏΡ€Π°Π²ΠΊΠ° ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ ΠΎΠ± отпискС + + Args: + _: None - Π‘Ρ‚Π°Π½Π΄Π°Ρ€Ρ‚Π½Ρ‹ΠΉ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ GraphQL (Π½Π΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ) + info: GraphQLResolveInfo - ΠšΠΎΠ½Ρ‚Π΅ΠΊΡΡ‚ GraphQL запроса, содСрТит ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ ΠΎΠ± Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·ΠΎΠ²Π°Π½Π½ΠΎΠΌ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ + what: str - Π’ΠΈΠΏ сущности для отписки ('AUTHOR', 'TOPIC', 'COMMUNITY', 'SHOUT') + slug: str - Slug сущности (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, 'author-slug' ΠΈΠ»ΠΈ 'topic-slug') + entity_id: int | None - ID сущности (Π°Π»ΡŒΡ‚Π΅Ρ€Π½Π°Ρ‚ΠΈΠ²Π° slug) + + Returns: + dict[str, Any] - Π Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ: + { + "success": bool, # Π£ΡΠΏΠ΅ΡˆΠ½ΠΎΡΡ‚ΡŒ ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ + "error": str | None, # ВСкст ошибки Ссли Π΅ΡΡ‚ΡŒ + "authors": Author[], # ΠžΠ±Π½ΠΎΠ²Π»Π΅Π½Π½Ρ‹Π΅ Π°Π²Ρ‚ΠΎΡ€Ρ‹ (для ΠΊΠ΅ΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡ) + "topics": Topic[], # ΠžΠ±Π½ΠΎΠ²Π»Π΅Π½Π½Ρ‹Π΅ Ρ‚Π΅ΠΌΡ‹ (для ΠΊΠ΅ΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡ) + } + + Raises: + ValueError: ΠŸΡ€ΠΈ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‡Π΅ Π½Π΅ΠΊΠΎΡ€Ρ€Π΅ΠΊΡ‚Π½Ρ‹Ρ… ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ΠΎΠ² + DatabaseError: ΠŸΡ€ΠΈ ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΠ°Ρ… с Π±Π°Π·ΠΎΠΉ Π΄Π°Π½Π½Ρ‹Ρ… + + Examples: + # ΠžΡ‚ΠΏΠΈΡΠΊΠ° ΠΎΡ‚ Π°Π²Ρ‚ΠΎΡ€Π° + mutation { + unfollow(what: "AUTHOR", slug: "author-slug") { + success + error + } + } + + # ΠžΡ‚ΠΏΠΈΡΠΊΠ° ΠΎΡ‚ Ρ‚Π΅ΠΌΡ‹ + mutation { + unfollow(what: "TOPIC", slug: "topic-slug") { + success + error + } + } + + # ΠžΡ‚ΠΏΠΈΡΠΊΠ° ΠΎΡ‚ сообщСства + mutation { + unfollow(what: "COMMUNITY", slug: "community-slug") { + success + error + } + } + + # ΠžΡ‚ΠΏΠΈΡΠΊΠ° ΠΎΡ‚ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ + mutation { + unfollow(what: "SHOUT", entity_id: 123) { + success + error + } + } + """ logger.debug("Начало выполнСния Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ 'unfollow'") viewer_id = info.context.get("author", {}).get("id") - if not viewer_id: - return {"error": "Access denied"} follower_dict = info.context.get("author") or {} + + # βœ… КРИВИЧНО: Π˜Π½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ кСш Π’ БАМОМ НАЧАЛЕ, Ссли ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·ΠΎΠ²Π°Π½ + # Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΏΡ€Π΅Π΄ΠΎΡ‚Π²Ρ€Π°Ρ‚ΠΈΡ‚ΡŒ Ρ‡Ρ‚Π΅Π½ΠΈΠ΅ старых Π΄Π°Π½Π½Ρ‹Ρ… ΠΏΡ€ΠΈ ΠΏΠΎΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅ΠΉ ΠΏΠ΅Ρ€Π΅Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ΅ + if viewer_id: + entity_type = what.lower() + cache_key_pattern = f"author:follows-{entity_type}s:{viewer_id}" + await redis.execute("DEL", cache_key_pattern) + await redis.execute("DEL", f"author:id:{viewer_id}") + logger.debug(f"Π˜Π½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΠΎΠ²Π°Π½ кСш подписок Π’ НАЧАЛЕ ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ unfollow: {cache_key_pattern}") + + # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·Π°Ρ†ΠΈΠΈ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ + if not viewer_id: + logger.warning("ΠŸΠΎΠΏΡ‹Ρ‚ΠΊΠ° ΠΎΡ‚ΠΏΠΈΡΠ°Ρ‚ΡŒΡΡ Π±Π΅Π· Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·Π°Ρ†ΠΈΠΈ") + return {"error": "Access denied"} + logger.debug(f"follower: {follower_dict}") if not viewer_id or not follower_dict: @@ -205,6 +364,7 @@ async def unfollow( follower_id = follower_dict.get("id") logger.debug(f"follower_id: {follower_id}") + # Маппинг Ρ‚ΠΈΠΏΠΎΠ² сущностСй Π½Π° ΠΈΡ… классы ΠΈ ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹ ΠΊΠ΅ΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡ entity_classes = { "AUTHOR": (Author, AuthorFollower, get_cached_follower_authors, cache_author), "TOPIC": (Topic, TopicFollower, get_cached_follower_topics, cache_topic), @@ -262,11 +422,7 @@ async def unfollow( session.commit() logger.info(f"ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ {follower_id} отписался ΠΎΡ‚ {what.lower()} с ID {entity_id}") - # Π˜Π½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ кэш подписок ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ - cache_key_pattern = f"author:follows-{entity_type}s:{follower_id}" - await redis.execute("DEL", cache_key_pattern) - logger.debug(f"Π˜Π½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΠΎΠ²Π°Π½ кэш подписок: {cache_key_pattern}") - + # КСш подписок follower'Π° ΡƒΠΆΠ΅ ΠΈΠ½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΠΎΠ²Π°Π½ Π² Π½Π°Ρ‡Π°Π»Π΅ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ if get_cached_follows_method and isinstance(follower_id, int): logger.debug("ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ Π°ΠΊΡ‚ΡƒΠ°Π»ΡŒΠ½ΠΎΠ³ΠΎ списка подписок ΠΈΠ· кэша") follows = await get_cached_follows_method(follower_id) @@ -277,6 +433,11 @@ async def unfollow( if what == "AUTHOR" and isinstance(follower_dict, dict): await notify_follower(follower=follower_dict, author_id=entity_id, action="unfollow") + # βœ… КРИВИЧНО: Π˜Π½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ кСш списка подписчиков Π°Π²Ρ‚ΠΎΡ€Π° + # Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΎΡ‚ΠΏΠΈΡΠ°Π²ΡˆΠΈΠΉΡΡ сразу исчСз ΠΈΠ· списка + await redis.execute("DEL", f"author:followers:{entity_id}") + logger.debug(f"Π˜Π½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΠΎΠ²Π°Π½ кСш подписчиков Π°Π²Ρ‚ΠΎΡ€Π° послС unfollow: author:followers:{entity_id}") + # Π˜Π½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ кСш статистики Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² для обновлСния счСтчиков подписчиков logger.debug("Π˜Π½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ кСш статистики Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² послС отписки") await invalidate_authors_cache(entity_id) diff --git a/resolvers/notifier.py b/resolvers/notifier.py index 131454b2..e8d85fbf 100644 --- a/resolvers/notifier.py +++ b/resolvers/notifier.py @@ -16,7 +16,7 @@ from orm.notification import ( NotificationEntity, NotificationSeen, ) -from orm.shout import Shout +from orm.shout import Shout, ShoutReactionsFollower from services.auth import login_required from storage.db import local_session from storage.schema import mutation, query @@ -57,6 +57,37 @@ def query_notifications(author_id: int, after: int = 0) -> tuple[int, int, list[ return total, unread, notifications +def check_subscription(shout_id: int, current_author_id: int) -> bool: + """ + ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅Ρ‚ подписку ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π½Π° увСдомлСния ΠΎ ΡˆΠ°ΡƒΡ‚Π΅. + + ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅Ρ‚ Π½Π°Π»ΠΈΡ‡ΠΈΠ΅ записи Π² ShoutReactionsFollower: + - Π—Π°ΠΏΠΈΡΡŒ Π΅ΡΡ‚ΡŒ β†’ подписан + - Записи Π½Π΅Ρ‚ β†’ Π½Π΅ подписан (отписался ΠΈΠ»ΠΈ Π½ΠΈΠΊΠΎΠ³Π΄Π° Π½Π΅ подписывался) + + АвтоматичСская подписка (auto=True) создаСтся ΠΏΡ€ΠΈ: + - Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠΈ поста + - ΠŸΠ΅Ρ€Π²ΠΎΠΌ ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ/Ρ€Π΅Π°ΠΊΡ†ΠΈΠΈ + + ΠžΡ‚ΠΏΠΈΡΠΊΠ° = ΡƒΠ΄Π°Π»Π΅Π½ΠΈΠ΅ записи ΠΈΠ· Ρ‚Π°Π±Π»ΠΈΡ†Ρ‹ + + Returns: + bool: True Ссли подписан Π½Π° увСдомлСния + """ + with local_session() as session: + # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π½Π°Π»ΠΈΡ‡ΠΈΠ΅ записи Π² ShoutReactionsFollower + follow = ( + session.query(ShoutReactionsFollower) + .filter( + ShoutReactionsFollower.follower == current_author_id, + ShoutReactionsFollower.shout == shout_id, + ) + .first() + ) + + return follow is not None + + def group_notification( thread: str, authors: list[Any] | None = None, @@ -105,7 +136,6 @@ def get_notifications_grouped(author_id: int, after: int = 0, limit: int = 10, o authors: List[NotificationAuthor], # List of authors involved in the thread. } """ - # TODO: use all stats _total, _unread, notifications = query_notifications(author_id, after) groups_by_thread = {} groups_amount = 0 @@ -119,14 +149,20 @@ def get_notifications_grouped(author_id: int, after: int = 0, limit: int = 10, o if str(notification.entity) == NotificationEntity.SHOUT.value: shout = payload shout_id = shout.get("id") - author_id = shout.get("created_by") + shout_author_id = shout.get("created_by") thread_id = f"shout-{shout_id}" + with local_session() as session: - author = session.query(Author).where(Author.id == author_id).first() + author = session.query(Author).where(Author.id == shout_author_id).first() shout = session.query(Shout).where(Shout.id == shout_id).first() if author and shout: + # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ подписку - Ссли Π½Π΅ подписан, пропускаСм это ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠ΅ + if not check_subscription(shout_id, author_id): + continue + author_dict = author.dict() shout_dict = shout.dict() + group = group_notification( thread_id, shout=shout_dict, @@ -154,7 +190,8 @@ def get_notifications_grouped(author_id: int, after: int = 0, limit: int = 10, o reply_id = reaction.get("reply_to") thread_id = f"shout-{shout_id}" if reply_id and reaction.get("kind", "").lower() == "comment": - thread_id += f"{reply_id}" + thread_id = f"shout-{shout_id}::{reply_id}" + existing_group = groups_by_thread.get(thread_id) if existing_group: existing_group["seen"] = False @@ -163,6 +200,10 @@ def get_notifications_grouped(author_id: int, after: int = 0, limit: int = 10, o existing_group["reactions"].append(reaction) groups_by_thread[thread_id] = existing_group else: + # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ подписку - Ссли Π½Π΅ подписан, пропускаСм это ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠ΅ + if not check_subscription(shout_id, author_id): + continue + group = group_notification( thread_id, authors=[author_dict], @@ -214,6 +255,10 @@ async def load_notifications(_: None, info: GraphQLResolveInfo, after: int, limi if author_id: groups_list = get_notifications_grouped(author_id, after, limit) notifications = sorted(groups_list, key=lambda group: group.get("updated_at", 0), reverse=True) + + # Π‘Ρ‡ΠΈΡ‚Π°Π΅ΠΌ Ρ€Π΅Π°Π»ΡŒΠ½ΠΎΠ΅ количСство сгруппированных ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ + total = len(notifications) + unread = sum(1 for n in notifications if not n.get("seen", False)) except Exception as e: error = str(e) logger.error(e) @@ -245,7 +290,7 @@ async def notification_mark_seen(_: None, info: GraphQLResolveInfo, notification @mutation.field("notifications_seen_after") @login_required async def notifications_seen_after(_: None, info: GraphQLResolveInfo, after: int) -> dict: - # TODO: use latest loaded notification_id as input offset parameter + """Mark all notifications after given timestamp as seen.""" error = None try: author_id = info.context.get("author", {}).get("id") @@ -273,18 +318,64 @@ async def notifications_seen_thread(_: None, info: GraphQLResolveInfo, thread: s error = None author_id = info.context.get("author", {}).get("id") if author_id: - [shout_id, reply_to_id] = thread.split(":") with local_session() as session: # Convert Unix timestamp to datetime for PostgreSQL compatibility after_datetime = datetime.fromtimestamp(after, tz=UTC) if after else None - # TODO: handle new follower and new shout notifications + # Handle different thread types: shout reactions, followers, or new shouts + if thread == "followers": + # Mark follower notifications as seen + query_conditions = [ + Notification.entity == NotificationEntity.AUTHOR.value, + ] + if after_datetime: + query_conditions.append(Notification.created_at > after_datetime) + + follower_notifications = session.query(Notification).where(and_(*query_conditions)).all() + for n in follower_notifications: + try: + ns = NotificationSeen(notification=n.id, viewer=author_id) + session.add(ns) + except Exception as e: + logger.warning(f"Failed to mark follower notification as seen: {e}") + session.commit() + return {"error": None} + + # Handle shout and reaction notifications + thread_parts = thread.split(":") + if len(thread_parts) < 2: + return {"error": "Invalid thread format"} + + shout_id = thread_parts[0] + reply_to_id = thread_parts[1] if len(thread_parts) > 1 else None + + # Query for new shout notifications in this thread + shout_query_conditions = [ + Notification.entity == NotificationEntity.SHOUT.value, + Notification.action == NotificationAction.CREATE.value, + ] + if after_datetime: + shout_query_conditions.append(Notification.created_at > after_datetime) + + shout_notifications = session.query(Notification).where(and_(*shout_query_conditions)).all() + + # Mark relevant shout notifications as seen + for n in shout_notifications: + payload = orjson.loads(str(n.payload)) + if str(payload.get("id")) == shout_id: + try: + ns = NotificationSeen(notification=n.id, viewer=author_id) + session.add(ns) + except Exception as e: + logger.warning(f"Failed to mark shout notification as seen: {e}") + + # Query for reaction notifications if after_datetime: new_reaction_notifications = ( session.query(Notification) .where( - Notification.action == "create", - Notification.entity == "reaction", + Notification.action == NotificationAction.CREATE.value, + Notification.entity == NotificationEntity.REACTION.value, Notification.created_at > after_datetime, ) .all() @@ -292,8 +383,8 @@ async def notifications_seen_thread(_: None, info: GraphQLResolveInfo, thread: s removed_reaction_notifications = ( session.query(Notification) .where( - Notification.action == "delete", - Notification.entity == "reaction", + Notification.action == NotificationAction.DELETE.value, + Notification.entity == NotificationEntity.REACTION.value, Notification.created_at > after_datetime, ) .all() @@ -302,16 +393,16 @@ async def notifications_seen_thread(_: None, info: GraphQLResolveInfo, thread: s new_reaction_notifications = ( session.query(Notification) .where( - Notification.action == "create", - Notification.entity == "reaction", + Notification.action == NotificationAction.CREATE.value, + Notification.entity == NotificationEntity.REACTION.value, ) .all() ) removed_reaction_notifications = ( session.query(Notification) .where( - Notification.action == "delete", - Notification.entity == "reaction", + Notification.action == NotificationAction.DELETE.value, + Notification.entity == NotificationEntity.REACTION.value, ) .all() ) diff --git a/resolvers/reaction.py b/resolvers/reaction.py index cae61bf8..5addf419 100644 --- a/resolvers/reaction.py +++ b/resolvers/reaction.py @@ -1,3 +1,4 @@ +import asyncio import time import traceback from typing import Any @@ -143,27 +144,29 @@ def is_featured_author(session: Session, author_id: int) -> bool: def check_to_feature(session: Session, approver_id: int, reaction: dict) -> bool: """ - Make a shout featured if it receives more than 4 votes from authors. + Make a shout featured if it receives more than 4 votes from featured authors. :param session: Database session. :param approver_id: Approver author ID. :param reaction: Reaction object. :return: True if shout should be featured, else False. """ - is_positive_kind = reaction.get("kind") == ReactionKind.LIKE.value + # πŸ”§ ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π»ΡŽΠ±ΡƒΡŽ ΠΏΠΎΠ»ΠΎΠΆΠΈΡ‚Π΅Π»ΡŒΠ½ΡƒΡŽ Ρ€Π΅Π°ΠΊΡ†ΠΈΡŽ (LIKE, ACCEPT, PROOF), Π½Π΅ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ LIKE + is_positive_kind = reaction.get("kind") in POSITIVE_REACTIONS if not reaction.get("reply_to") and is_positive_kind: # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, Π½Π΅ содСрТит Π»ΠΈ пост Π±ΠΎΠ»Π΅Π΅ 20% Π΄ΠΈΠ·Π»Π°ΠΉΠΊΠΎΠ² # Если Π΄Π°, Ρ‚ΠΎ Π½Π΅ Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ featured нСзависимо ΠΎΡ‚ количСства Π»Π°ΠΉΠΊΠΎΠ² if check_to_unfeature(session, reaction): return False - # Π‘ΠΎΠ±ΠΈΡ€Π°Π΅ΠΌ всСх Π°Π²Ρ‚ΠΎΡ€ΠΎΠ², ΠΏΠΎΡΡ‚Π°Π²ΠΈΠ²ΡˆΠΈΡ… Π»Π°ΠΉΠΊ + # Π‘ΠΎΠ±ΠΈΡ€Π°Π΅ΠΌ всСх Π°Π²Ρ‚ΠΎΡ€ΠΎΠ², ΠΏΠΎΡΡ‚Π°Π²ΠΈΠ²ΡˆΠΈΡ… ΠΏΠΎΠ»ΠΎΠΆΠΈΡ‚Π΅Π»ΡŒΠ½ΡƒΡŽ Ρ€Π΅Π°ΠΊΡ†ΠΈΡŽ author_approvers = set() reacted_readers = ( session.query(Reaction.created_by) .where( Reaction.shout == reaction.get("shout"), Reaction.kind.in_(POSITIVE_REACTIONS), + Reaction.reply_to.is_(None), # Π½Π΅ рСакция Π½Π° ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ # Π Π΅ΠΉΡ‚ΠΈΠ½Π³ΠΈ (LIKE, DISLIKE) физичСски ΡƒΠ΄Π°Π»ΡΡŽΡ‚ΡΡ, поэтому Ρ„ΠΈΠ»ΡŒΡ‚Ρ€ deleted_at Π½Π΅ Π½ΡƒΠΆΠ΅Π½ ) .distinct() @@ -189,7 +192,7 @@ def check_to_feature(session: Session, approver_id: int, reaction: dict) -> bool def check_to_unfeature(session: Session, reaction: dict) -> bool: """ Unfeature a shout if: - 1. Less than 5 positive votes, OR + 1. Less than 5 positive votes from featured authors, OR 2. 20% or more of reactions are negative. :param session: Database session. @@ -199,18 +202,8 @@ def check_to_unfeature(session: Session, reaction: dict) -> bool: if not reaction.get("reply_to"): shout_id = reaction.get("shout") - # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ ΡΠΎΠΎΡ‚Π½ΠΎΡˆΠ΅Π½ΠΈΠ΅ Π΄ΠΈΠ·Π»Π°ΠΉΠΊΠΎΠ², Π΄Π°ΠΆΠ΅ Ссли тСкущая рСакция Π½Π΅ Π΄ΠΈΠ·Π»Π°ΠΉΠΊ - total_reactions = ( - session.query(Reaction) - .where( - Reaction.shout == shout_id, - Reaction.reply_to.is_(None), - Reaction.kind.in_(RATING_REACTIONS), - # Π Π΅ΠΉΡ‚ΠΈΠ½Π³ΠΈ физичСски ΡƒΠ΄Π°Π»ΡΡŽΡ‚ΡΡ ΠΏΡ€ΠΈ ΡƒΠ΄Π°Π»Π΅Π½ΠΈΠΈ, поэтому Ρ„ΠΈΠ»ΡŒΡ‚Ρ€ deleted_at Π½Π΅ Π½ΡƒΠΆΠ΅Π½ - ) - .count() - ) - + # πŸ”§ Π‘Ρ‡ΠΈΡ‚Π°Π΅ΠΌ всС Ρ€Π΅ΠΉΡ‚ΠΈΠ½Π³ΠΎΠ²Ρ‹Π΅ Ρ€Π΅Π°ΠΊΡ†ΠΈΠΈ (ΠΏΠΎΠ»ΠΎΠΆΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ + ΠΎΡ‚Ρ€ΠΈΡ†Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹Π΅) + # Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ POSITIVE_REACTIONS + NEGATIVE_REACTIONS вмСсто Ρ‚ΠΎΠ»ΡŒΠΊΠΎ RATING_REACTIONS positive_reactions = ( session.query(Reaction) .where( @@ -233,9 +226,13 @@ def check_to_unfeature(session: Session, reaction: dict) -> bool: .count() ) + total_reactions = positive_reactions + negative_reactions + # УсловиС 1: МСньшС 5 голосов "Π·Π°" if positive_reactions < 5: - logger.debug(f"ΠŸΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΡ {shout_id}: {positive_reactions} Π»Π°ΠΉΠΊΠΎΠ² (мСньшС 5) - Π΄ΠΎΠ»ΠΆΠ½Π° Π±Ρ‹Ρ‚ΡŒ unfeatured") + logger.debug( + f"ΠŸΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΡ {shout_id}: {positive_reactions} ΠΏΠΎΠ»ΠΎΠΆΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… Ρ€Π΅Π°ΠΊΡ†ΠΈΠΉ (мСньшС 5) - Π΄ΠΎΠ»ΠΆΠ½Π° Π±Ρ‹Ρ‚ΡŒ unfeatured" + ) return True # УсловиС 2: ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, ΡΠΎΡΡ‚Π°Π²Π»ΡΡŽΡ‚ Π»ΠΈ ΠΎΡ‚Ρ€ΠΈΡ†Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ Ρ€Π΅Π°ΠΊΡ†ΠΈΠΈ 20% ΠΈΠ»ΠΈ Π±ΠΎΠ»Π΅Π΅ ΠΎΡ‚ всСх Ρ€Π΅Π°ΠΊΡ†ΠΈΠΉ @@ -256,6 +253,8 @@ async def set_featured(session: Session, shout_id: int) -> None: :param session: Database session. :param shout_id: Shout ID. """ + from cache.revalidator import revalidation_manager + s = session.query(Shout).where(Shout.id == shout_id).first() if s: current_time = int(time.time()) @@ -267,6 +266,22 @@ async def set_featured(session: Session, shout_id: int) -> None: session.add(s) session.commit() + # πŸ”§ РСвалидация кСша ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ ΠΈ связанных сущностСй + revalidation_manager.mark_for_revalidation(shout_id, "shouts") + # Π Π΅Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ + for author in s.authors: + revalidation_manager.mark_for_revalidation(author.id, "authors") + # Π Π΅Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ Ρ‚Π΅ΠΌΡ‹ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ + for topic in s.topics: + revalidation_manager.mark_for_revalidation(topic.id, "topics") + + # πŸ”§ Π˜Π½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ ΠΊΠ»ΡŽΡ‡ΠΈ кСша Π»Π΅Π½Ρ‚ для обновлСния featured статусов + from cache.cache import invalidate_shout_related_cache + + await invalidate_shout_related_cache(s, s.created_by) + + logger.info(f"ΠŸΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΡ {shout_id} ΠΏΠΎΠ»ΡƒΡ‡ΠΈΠ»Π° статус featured, кСш ΠΏΠΎΠΌΠ΅Ρ‡Π΅Π½ для Ρ€Π΅Π²Π°Π»ΠΈΠ΄Π°Ρ†ΠΈΠΈ") + def set_unfeatured(session: Session, shout_id: int) -> None: """ @@ -275,9 +290,33 @@ def set_unfeatured(session: Session, shout_id: int) -> None: :param session: Database session. :param shout_id: Shout ID. """ + from cache.revalidator import revalidation_manager + + # ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΡŽ для доступа ΠΊ Π°Π²Ρ‚ΠΎΡ€Π°ΠΌ ΠΈ Ρ‚Π΅ΠΌΠ°ΠΌ + shout = session.query(Shout).where(Shout.id == shout_id).first() + if not shout: + return + session.query(Shout).where(Shout.id == shout_id).update({"featured_at": None}) session.commit() + # πŸ”§ РСвалидация кСша ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ ΠΈ связанных сущностСй + revalidation_manager.mark_for_revalidation(shout_id, "shouts") + # Π Π΅Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ + for author in shout.authors: + revalidation_manager.mark_for_revalidation(author.id, "authors") + # Π Π΅Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ Ρ‚Π΅ΠΌΡ‹ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ + for topic in shout.topics: + revalidation_manager.mark_for_revalidation(topic.id, "topics") + + # πŸ”§ Π˜Π½Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ ΠΊΠ»ΡŽΡ‡ΠΈ кСша Π»Π΅Π½Ρ‚ для обновлСния featured статусов + from cache.cache import invalidate_shout_related_cache + + # Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ asyncio.create_task для асинхронного Π²Ρ‹Π·ΠΎΠ²Π° + asyncio.create_task(invalidate_shout_related_cache(shout, shout.created_by)) + + logger.info(f"ΠŸΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΡ {shout_id} потСряла статус featured, кСш ΠΏΠΎΠΌΠ΅Ρ‡Π΅Π½ для Ρ€Π΅Π²Π°Π»ΠΈΠ΄Π°Ρ†ΠΈΠΈ") + async def _create_reaction(session: Session, shout_id: int, is_author: bool, author_id: int, reaction: dict) -> dict: """ diff --git a/schema/mutation.graphql b/schema/mutation.graphql index 31ee060e..8ce76cc6 100644 --- a/schema/mutation.graphql +++ b/schema/mutation.graphql @@ -24,6 +24,7 @@ type Mutation { # draft create_draft(draft_input: DraftInput!): CommonResult! + create_draft_from_shout(shout_id: Int!): CommonResult! update_draft(draft_id: Int!, draft_input: DraftInput!): CommonResult! delete_draft(draft_id: Int!): CommonResult! # publication diff --git a/schema/type.graphql b/schema/type.graphql index 8097a0b9..165e5fd5 100644 --- a/schema/type.graphql +++ b/schema/type.graphql @@ -245,6 +245,7 @@ type AuthorFollowsResult { topics: [Topic] authors: [Author] communities: [Community] + shouts: [Shout] error: String } diff --git a/settings.py b/settings.py index d9d4984f..13b9cbbb 100644 --- a/settings.py +++ b/settings.py @@ -76,7 +76,7 @@ OAUTH_CLIENTS = { } # Настройки JWT -JWT_SECRET = os.getenv("JWT_SECRET", "your-secret-key") +JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key") JWT_ACCESS_TOKEN_EXPIRE_MINUTES = 30 JWT_REFRESH_TOKEN_EXPIRE_DAYS = int(environ.get("JWT_REFRESH_TOKEN_EXPIRE_DAYS", "30")) diff --git a/storage/env.py b/storage/env.py index 30b99034..5ffc8c9b 100644 --- a/storage/env.py +++ b/storage/env.py @@ -55,7 +55,7 @@ class EnvService: "POSTGRES_HOST": "database", "POSTGRES_PORT": "database", # Auth - "JWT_SECRET": "auth", + "JWT_SECRET_KEY": "auth", "JWT_ALGORITHM": "auth", "JWT_EXPIRATION": "auth", "SECRET_KEY": "auth", @@ -103,7 +103,7 @@ class EnvService: # Π‘Π΅ΠΊΡ€Π΅Ρ‚Π½Ρ‹Π΅ ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Π΅ (Π½Π΅ ΠΏΠΎΠΊΠ°Π·Ρ‹Π²Π°Π΅ΠΌ ΠΈΡ… значСния Π² UI) SECRET_VARIABLES: ClassVar[set[str]] = { - "JWT_SECRET", + "JWT_SECRET_KEY", "SECRET_KEY", "AUTH_SECRET", "OAUTH_GOOGLE_CLIENT_SECRET", @@ -127,7 +127,7 @@ class EnvService: "POSTGRES_DB": "Имя Π±Π°Π·Ρ‹ Π΄Π°Π½Π½Ρ‹Ρ… PostgreSQL", "POSTGRES_HOST": "Π₯ост PostgreSQL", "POSTGRES_PORT": "ΠŸΠΎΡ€Ρ‚ PostgreSQL", - "JWT_SECRET": "Π‘Π΅ΠΊΡ€Π΅Ρ‚Π½Ρ‹ΠΉ ΠΊΠ»ΡŽΡ‡ для JWT Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ²", + "JWT_SECRET_KEY": "Π‘Π΅ΠΊΡ€Π΅Ρ‚Π½Ρ‹ΠΉ ΠΊΠ»ΡŽΡ‡ для JWT Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ²", "JWT_ALGORITHM": "Алгоритм подписи JWT", "JWT_EXPIRATION": "ВрСмя ΠΆΠΈΠ·Π½ΠΈ JWT Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ²", "SECRET_KEY": "Π‘Π΅ΠΊΡ€Π΅Ρ‚Π½Ρ‹ΠΉ ΠΊΠ»ΡŽΡ‡ прилоТСния", diff --git a/utils/sentry.py b/utils/sentry.py index 6825003e..6944d227 100644 --- a/utils/sentry.py +++ b/utils/sentry.py @@ -64,12 +64,5 @@ def start_sentry() -> None: ) logger.info("[utils.sentry] Sentry initialized successfully.") - # πŸ§ͺ ΠžΡ‚ΠΏΡ€Π°Π²Π»ΡΠ΅ΠΌ тСстовоС событиС для ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ Ρ€Π°Π±ΠΎΡ‚Ρ‹ GlitchTip - try: - sentry_sdk.capture_message("πŸ§ͺ GlitchTip test message - систСма ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½Π°", level="info") - logger.info("[utils.sentry] ВСстовоС сообщСниС ΠΎΡ‚ΠΏΡ€Π°Π²Π»Π΅Π½ΠΎ Π² GlitchTip") - except Exception as test_e: - logger.warning(f"[utils.sentry] НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ тСстовоС сообщСниС: {test_e}") - except (sentry_sdk.utils.BadDsn, ImportError, ValueError, TypeError) as _e: logger.warning("[utils.sentry] Failed to initialize Sentry", exc_info=True) diff --git a/uv.lock b/uv.lock index 8c7d8a49..fb083f96 100644 --- a/uv.lock +++ b/uv.lock @@ -443,7 +443,7 @@ wheels = [ [[package]] name = "discours-core" -version = "0.9.28" +version = "0.9.32" source = { editable = "." } dependencies = [ { name = "ariadne" },