This commit is contained in:
2025-10-09 01:16:56 +03:00
32 changed files with 905 additions and 382 deletions

4
.gitignore vendored
View File

@@ -182,4 +182,6 @@ docs/progress/*
panel/graphql/generated
test_e2e.db*
test_e2e.db*
uv.lock

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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",

10
cache/cache.py vendored
View File

@@ -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))

View File

@@ -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

View File

@@ -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)
### Реализация
- [ ] Реализована функция извлечения токенов из запросов

View File

@@ -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

View File

@@ -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 дней (настраивается)

View File

@@ -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 настройки

View File

@@ -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 подключение

View File

@@ -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

View File

@@ -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_*

View File

@@ -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"

518
package-lock.json generated
View File

@@ -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": {

View File

@@ -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"
}
}

View File

@@ -69,7 +69,7 @@ export const AuthProvider: Component<AuthProviderProps> = (props) => {
// Начинаем с false чтобы избежать мерцания, реальная проверка будет в onMount
const [isAuthenticated, setIsAuthenticated] = createSignal(false)
const [isReady, setIsReady] = createSignal(false)
// Флаг для предотвращения повторных инициализаций
let isInitializing = false
@@ -82,7 +82,7 @@ export const AuthProvider: Component<AuthProviderProps> = (props) => {
console.log('[AuthProvider] Already initializing, skipping...')
return
}
isInitializing = true
console.log('[AuthProvider] Performing auth initialization...')
@@ -91,7 +91,7 @@ export const AuthProvider: Component<AuthProviderProps> = (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<AuthProviderProps> = (props) => {
const logout = async () => {
console.log('[AuthProvider] Attempting logout...')
// Предотвращаем повторные инициализации во время logout
isInitializing = true
try {
// Сначала очищаем токены на клиенте
clearAuthTokens()

View File

@@ -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'

View File

@@ -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"}

View File

@@ -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:

View File

@@ -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,
}

View File

@@ -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])

View File

@@ -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)

View File

@@ -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()
)

View File

@@ -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:
"""

View File

@@ -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

View File

@@ -245,6 +245,7 @@ type AuthorFollowsResult {
topics: [Topic]
authors: [Author]
communities: [Community]
shouts: [Shout]
error: String
}

View File

@@ -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"))

View File

@@ -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": "Секретный ключ приложения",

View File

@@ -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)

2
uv.lock generated
View File

@@ -443,7 +443,7 @@ wheels = [
[[package]]
name = "discours-core"
version = "0.9.28"
version = "0.9.32"
source = { editable = "." }
dependencies = [
{ name = "ariadne" },