Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df9a269d76 | ||
|
|
da7326ffde | ||
|
|
3a1276596c | ||
|
|
07904f05e5 |
@@ -1,5 +1,99 @@
|
|||||||
# Changelog - FlowDeck
|
# Changelog - FlowDeck
|
||||||
|
|
||||||
|
## v7.12.0 (2026-10-01) — Audit : A32 phase 2a (library 10/10)
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- `library.py` passe de **1/10 à 8 routes couvertes** : les 5 listes
|
||||||
|
(recents/favorites/published/private/workspace) en un test de boucle,
|
||||||
|
`/private` avec une page seedée et retrouvée, `/children/{id}` avec un
|
||||||
|
parent/enfant seedés (titre retrouvé), `/repository` vide et clé
|
||||||
|
(aucun appel réseau — la clé n'est qu'une string de workspace)
|
||||||
|
- Test de non-régression 404 sur les 2 routes supprimées
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- **2 routes cassées supprimées** (découverte des smokes) :
|
||||||
|
`/api/library/local-workspace-children/{id}` renvoyait un 500 systématique
|
||||||
|
et `/api/library/local-workspace` un 500 dès qu'un workspace existait —
|
||||||
|
les deux lisaient `local_workspace_items`, **une table qui n'est créée nulle
|
||||||
|
part** dans le codebase (grep : 0 `CREATE TABLE`), avec **0 référence front**.
|
||||||
|
`library._format_size` devenu mort : supprimé aussi (une version vit dans
|
||||||
|
`dashboard.py`, inchangée)
|
||||||
|
- `local_workspace_items` : plus aucune occurrence dans `app/`
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- Suite complète : **1053/1053** (229 s) ; `test_smoke_uncovered.py` : 16 tests
|
||||||
|
|
||||||
|
## v7.11.0 (2026-10-01) — Audit : A32 phase 1 (routers à 0 test)
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- `tests/test_smoke_uncovered.py` — **10 smoke tests**, un par route des 4
|
||||||
|
routers qui n'avaient AUCUN test :
|
||||||
|
· `webhooks.py` (3/3) : réception sans secret → `{"status":"ok"}` ;
|
||||||
|
HMAC faux → 401 (secret piloté par monkeypatch) ; register sans secret →
|
||||||
|
400 **avant** tout appel réseau ; status avec `gitea.list_webhooks` stubbé
|
||||||
|
(zéro accès réseau réel)
|
||||||
|
· `notes.py` (2/2) : GET HTML + roundtrip POST→GET (upsert persisté,
|
||||||
|
échappement HTML vérifié `<b>`)
|
||||||
|
· `sidebar_config.py` (2/2) : GET défauts, PUT persisté relu depuis
|
||||||
|
`users.sidebar_config`, 400 sans `config`, remise en état en fin de test
|
||||||
|
· `github_routes.py` (2/2) : status `{"linked": False}`, disconnect ok
|
||||||
|
- Reste (phase 2) : quasi nuls — `library.py` 1/10, `api.py` 3/23,
|
||||||
|
`dashboard.py` 17/63, `api_v2.py` 50/115
|
||||||
|
- Suite complète : **1047/1047**
|
||||||
|
|
||||||
|
## v7.10.0 (2026-10-01) — Audit : A21 phase 2b (api_v2 bouclé)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Helper** `run_event_sync(coro)` (`app/services/automations.py`) : exécute
|
||||||
|
une coroutine d'événement depuis un handler synchrone — `asyncio.run` sur une
|
||||||
|
boucle dédiée dans le **worker threadpool** : bloqué = le worker, jamais la
|
||||||
|
boucle d'event, et la réponse n'est envoyée qu'une fois l'événement terminé
|
||||||
|
(déterministe, équivalent sémantique de l'`await` d'avant). Note
|
||||||
|
`ponytail:` : clients httpx créés à chaque appel partout → aucun lien de
|
||||||
|
boucle ; sinon `run_coroutine_threadsafe` + boucle du lifespan
|
||||||
|
- **A21 (phase 2b)** — 15 routes `api_v2` dont les seuls awaits étaient
|
||||||
|
`request.json` / `_fire_event` / `fire_published` / `fire_unpublished` →
|
||||||
|
paramètre `Body(default={})` + `run_event_sync(...)` + conversion en `def`
|
||||||
|
- **`api_v2` : 111/115 routes hors event loop** — il ne reste que 4 routes
|
||||||
|
async, toutes avec de vrais awaits réseau : `import_csv_v2` (multipart),
|
||||||
|
`project_tree_v2` (gitea), `test_webhook_v2`, `retry_webhook_deliveries`
|
||||||
|
- Repo-wide : **403 routes synchrones (hors loop) / 260 async** (phase 2c)
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- Ciblé (public_api_v2 + v65 + webhooks_v2 + audit) : 90/90 — les webhooks
|
||||||
|
prouvent la détermination de `run_event_sync` ; suite complète **1037/1037**
|
||||||
|
en 228 s
|
||||||
|
|
||||||
|
## v7.9.0 (2026-10-01) — Audit : A21 phase 2a (api_v2 hors loop)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **A21 (phase 2a)** — dans `api_v2`, les 36 routes dont le **seul** `await`
|
||||||
|
était `body = await request.json()` passent à un paramètre FastAPI
|
||||||
|
`body: dict = Body(default={})` (parsing async fait par FastAPI avant
|
||||||
|
l'appel) puis sont converties en `def` → threadpool. Équivalences vérifiées
|
||||||
|
avant-engagement : corps absent → `{}` (identique au `try/except` d'avant),
|
||||||
|
JSON invalide → **422** (avant : traité silencieusement comme `{}`),
|
||||||
|
zéro `body[...] = ` dans le fichier (le défaut partagé n'est jamais muté)
|
||||||
|
- `api_v2` : **96/115 routes hors event loop** (60 en phase 1 + 36 ici) ;
|
||||||
|
il ne reste que **19 routes async** dans ce router (`fire_event`,
|
||||||
|
`request.form`, appels gitea/webhooks — phase 2b)
|
||||||
|
- Bug de transformation évité en cours de route : première version du script
|
||||||
|
supprimait 5 lignes au lieu de 4 (`slice` fermant d'un cran trop loin) —
|
||||||
|
fichier restauré depuis git puis script corrigé, 0 ligne perdue (diff
|
||||||
|
logique : +39/-183 = 36 signatures + import, 4 lignes de try/except × 35)
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- Suite complète **1037/1037** (242 s) ; ciblée sur `test_public_api_v2` +
|
||||||
|
`test_v65` + audit : 62/62 verts avant la passe complète
|
||||||
|
|
||||||
## v7.8.0 (2026-10-01) — Audit : A21 phase 1 (SQLite hors event loop)
|
## v7.8.0 (2026-10-01) — Audit : A21 phase 1 (SQLite hors event loop)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
+3
-3
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# WORKLOAD — FlowDeck Notion Clone
|
# WORKLOAD — FlowDeck Notion Clone
|
||||||
|
|
||||||
> **Début**: 2026-07-08 | **Version**: v7.8.0 (audit — A21 phase 1 : 352 routes hors loop) | **Statut**: EN COURS 🔄
|
> **Début**: 2026-07-08 | **Version**: v7.12.0 (audit — A32 phase 2 : library 10/10, 2 routes mortes supprimées) | **Statut**: EN COURS 🔄
|
||||||
> **Cible**: parité Notion + intégration forge · **Follow-ups v7.3 livrés**: sidebar teamspaces, notif `page.updated`, charts `number` + dashboards multi-DB, unfurl forge, UI Settings → Audit — voir `ROADMAP.md § v7.3.0`
|
> **Cible**: parité Notion + intégration forge · **Follow-ups v7.3 livrés**: sidebar teamspaces, notif `page.updated`, charts `number` + dashboards multi-DB, unfurl forge, UI Settings → Audit — voir `ROADMAP.md § v7.3.0`
|
||||||
|
|
||||||
## Avancement Global
|
## Avancement Global
|
||||||
|
|||||||
+1
-1
@@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI):
|
|||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="FlowDeck",
|
title="FlowDeck",
|
||||||
version="7.8.0",
|
version="7.12.0",
|
||||||
docs_url="/docs",
|
docs_url="/docs",
|
||||||
redoc_url="/redoc",
|
redoc_url="/redoc",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
|
|||||||
+70
-257
@@ -12,7 +12,7 @@ import logging
|
|||||||
import secrets
|
import secrets
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from fastapi import APIRouter, Header, HTTPException, Request
|
from fastapi import APIRouter, Body, Header, HTTPException, Request
|
||||||
from fastapi.responses import JSONResponse, Response
|
from fastapi.responses import JSONResponse, Response
|
||||||
|
|
||||||
from app.db import get_conn
|
from app.db import get_conn
|
||||||
@@ -31,6 +31,7 @@ from app.services.api_v2_helpers import ( # noqa: F401 — require_scope est ut
|
|||||||
validate_scopes_input,
|
validate_scopes_input,
|
||||||
)
|
)
|
||||||
from app.services.automations import fire_event as _fire_event
|
from app.services.automations import fire_event as _fire_event
|
||||||
|
from app.services.automations import run_event_sync
|
||||||
from app.services.publish import fire_published, fire_unpublished, publish, unpublish
|
from app.services.publish import fire_published, fire_unpublished, publish, unpublish
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -48,13 +49,9 @@ def _v2_rate_check(request: Request, user: dict) -> None:
|
|||||||
# ── Tokens ────────────────────────────────────────────────────────────────
|
# ── Tokens ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@router.post("/tokens")
|
@router.post("/tokens")
|
||||||
async def create_token(request: Request, authorization: str | None = Header(default=None)):
|
def create_token(request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
name = (body.get("name") or "API token").strip()[:100]
|
name = (body.get("name") or "API token").strip()[:100]
|
||||||
scopes = validate_scopes_input(body.get("scopes") or "read,write")
|
scopes = validate_scopes_input(body.get("scopes") or "read,write")
|
||||||
expires_at = body.get("expires_at")
|
expires_at = body.get("expires_at")
|
||||||
@@ -166,13 +163,9 @@ def get_me(request: Request, authorization: str | None = Header(default=None)):
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
@router.patch("/users/me")
|
@router.patch("/users/me")
|
||||||
async def patch_me(request: Request, authorization: str | None = Header(default=None)):
|
def patch_me(request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
allowed = {"full_name", "email", "avatar_color", "notification_prefs", "sidebar_config", "timezone"}
|
allowed = {"full_name", "email", "avatar_color", "notification_prefs", "sidebar_config", "timezone"}
|
||||||
updates = {}
|
updates = {}
|
||||||
for k in allowed:
|
for k in allowed:
|
||||||
@@ -232,16 +225,12 @@ def list_workspaces(request: Request, authorization: str | None = Header(default
|
|||||||
return {"workspaces": out, "total": total, "limit": limit, "offset": offset}
|
return {"workspaces": out, "total": total, "limit": limit, "offset": offset}
|
||||||
|
|
||||||
@router.post("/workspaces")
|
@router.post("/workspaces")
|
||||||
async def create_workspace(request: Request, authorization: str | None = Header(default=None)):
|
def create_workspace(request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
idem = check_idempotency(request, user["id"])
|
idem = check_idempotency(request, user["id"])
|
||||||
if idem:
|
if idem:
|
||||||
return JSONResponse(content=idem["data"], status_code=idem["status"])
|
return JSONResponse(content=idem["data"], status_code=idem["status"])
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
name = (body.get("name") or "").strip()
|
name = (body.get("name") or "").strip()
|
||||||
if not name:
|
if not name:
|
||||||
raise HTTPException(400, "name is required")
|
raise HTTPException(400, "name is required")
|
||||||
@@ -282,13 +271,9 @@ def get_workspace(workspace_id: int, request: Request, authorization: str | None
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
@router.patch("/workspaces/{workspace_id}")
|
@router.patch("/workspaces/{workspace_id}")
|
||||||
async def patch_workspace(workspace_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def patch_workspace(workspace_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
row = conn.execute("SELECT * FROM workspaces WHERE id=?", (workspace_id,)).fetchone()
|
row = conn.execute("SELECT * FROM workspaces WHERE id=?", (workspace_id,)).fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
@@ -336,13 +321,9 @@ def list_workspace_members(workspace_id: int, request: Request, authorization: s
|
|||||||
return {"members": [row_to_dict(r) for r in rows]}
|
return {"members": [row_to_dict(r) for r in rows]}
|
||||||
|
|
||||||
@router.post("/workspaces/{workspace_id}/members")
|
@router.post("/workspaces/{workspace_id}/members")
|
||||||
async def invite_member(workspace_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def invite_member(workspace_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
target_id = body.get("user_id") or body.get("uid")
|
target_id = body.get("user_id") or body.get("uid")
|
||||||
email = (body.get("email") or "").strip()
|
email = (body.get("email") or "").strip()
|
||||||
role = (body.get("role") or "editor").strip().lower()
|
role = (body.get("role") or "editor").strip().lower()
|
||||||
@@ -374,13 +355,9 @@ async def invite_member(workspace_id: int, request: Request, authorization: str
|
|||||||
return {"workspace_id": workspace_id, "user_id": uid, "role": role, "status": "added"}
|
return {"workspace_id": workspace_id, "user_id": uid, "role": role, "status": "added"}
|
||||||
|
|
||||||
@router.patch("/workspaces/{workspace_id}/members/{uid}")
|
@router.patch("/workspaces/{workspace_id}/members/{uid}")
|
||||||
async def update_member_role(workspace_id: int, uid: int, request: Request, authorization: str | None = Header(default=None)):
|
def update_member_role(workspace_id: int, uid: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
role = (body.get("role") or "").strip().lower()
|
role = (body.get("role") or "").strip().lower()
|
||||||
if role not in ("owner", "admin", "editor", "viewer", "commenter"):
|
if role not in ("owner", "admin", "editor", "viewer", "commenter"):
|
||||||
raise HTTPException(400, "Invalid role")
|
raise HTTPException(400, "Invalid role")
|
||||||
@@ -451,16 +428,12 @@ def list_collections_v2(request: Request, authorization: str | None = Header(def
|
|||||||
return JSONResponse(content=resp, headers=paginate_headers(total))
|
return JSONResponse(content=resp, headers=paginate_headers(total))
|
||||||
|
|
||||||
@router.post("/collections")
|
@router.post("/collections")
|
||||||
async def create_collection_v2(request: Request, authorization: str | None = Header(default=None)):
|
def create_collection_v2(request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
idem = check_idempotency(request, user["id"])
|
idem = check_idempotency(request, user["id"])
|
||||||
if idem:
|
if idem:
|
||||||
return JSONResponse(content=idem["data"], status_code=idem["status"])
|
return JSONResponse(content=idem["data"], status_code=idem["status"])
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
name = (body.get("name") or "").strip()
|
name = (body.get("name") or "").strip()
|
||||||
if not name:
|
if not name:
|
||||||
raise HTTPException(400, "name is required")
|
raise HTTPException(400, "name is required")
|
||||||
@@ -507,13 +480,9 @@ def get_collection_v2(collection_id: int, request: Request, authorization: str |
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
@router.patch("/collections/{collection_id}")
|
@router.patch("/collections/{collection_id}")
|
||||||
async def patch_collection_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def patch_collection_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
row = conn.execute("SELECT * FROM collections WHERE id=?", (collection_id,)).fetchone()
|
row = conn.execute("SELECT * FROM collections WHERE id=?", (collection_id,)).fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
@@ -545,13 +514,9 @@ def delete_collection_v2(collection_id: int, request: Request, authorization: st
|
|||||||
return {"id": collection_id, "status": "deleted"}
|
return {"id": collection_id, "status": "deleted"}
|
||||||
|
|
||||||
@router.post("/collections/{collection_id}/linked")
|
@router.post("/collections/{collection_id}/linked")
|
||||||
async def create_linked_db(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def create_linked_db(collection_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
name = (body.get("name") or "").strip() or f"Linked DB {collection_id}"
|
name = (body.get("name") or "").strip() or f"Linked DB {collection_id}"
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
src = conn.execute("SELECT * FROM collections WHERE id=?", (collection_id,)).fetchone()
|
src = conn.execute("SELECT * FROM collections WHERE id=?", (collection_id,)).fetchone()
|
||||||
@@ -607,13 +572,9 @@ def list_sources(collection_id: int, request: Request, authorization: str | None
|
|||||||
return {"sources": [dict(r) for r in rows]}
|
return {"sources": [dict(r) for r in rows]}
|
||||||
|
|
||||||
@router.post("/collections/{collection_id}/sources")
|
@router.post("/collections/{collection_id}/sources")
|
||||||
async def add_source(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def add_source(collection_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
src_id = body.get("source_collection_id") or body.get("source_id")
|
src_id = body.get("source_collection_id") or body.get("source_id")
|
||||||
if not src_id:
|
if not src_id:
|
||||||
raise HTTPException(400, "source_collection_id required")
|
raise HTTPException(400, "source_collection_id required")
|
||||||
@@ -701,16 +662,12 @@ def list_collection_pages_v2(collection_id: int, request: Request, authorization
|
|||||||
return JSONResponse(content={"pages": out, "total": total, "limit": limit, "offset": offset}, headers=paginate_headers(total))
|
return JSONResponse(content={"pages": out, "total": total, "limit": limit, "offset": offset}, headers=paginate_headers(total))
|
||||||
|
|
||||||
@router.post("/collections/{collection_id}/pages")
|
@router.post("/collections/{collection_id}/pages")
|
||||||
async def create_collection_page_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def create_collection_page_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
idem = check_idempotency(request, user["id"])
|
idem = check_idempotency(request, user["id"])
|
||||||
if idem:
|
if idem:
|
||||||
return JSONResponse(content=idem["data"], status_code=idem["status"])
|
return JSONResponse(content=idem["data"], status_code=idem["status"])
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
title = (body.get("title") or body.get("name") or "Untitled").strip() or "Untitled"
|
title = (body.get("title") or body.get("name") or "Untitled").strip() or "Untitled"
|
||||||
icon = body.get("icon", "file")
|
icon = body.get("icon", "file")
|
||||||
parent_id = body.get("parent_id")
|
parent_id = body.get("parent_id")
|
||||||
@@ -743,7 +700,7 @@ async def create_collection_page_v2(collection_id: int, request: Request, author
|
|||||||
row = conn.execute("SELECT * FROM collection_pages WHERE id=?", (pid,)).fetchone()
|
row = conn.execute("SELECT * FROM collection_pages WHERE id=?", (pid,)).fetchone()
|
||||||
audit_log(user, "page.create", "collection_page", pid, title, request)
|
audit_log(user, "page.create", "collection_page", pid, title, request)
|
||||||
try:
|
try:
|
||||||
await _fire_event("collection.page.created", {"page_id": pid, "collection_id": collection_id, "title": title})
|
run_event_sync(_fire_event("collection.page.created", {"page_id": pid, "collection_id": collection_id, "title": title}))
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("create_collection_page_v2")
|
logger.exception("create_collection_page_v2")
|
||||||
data = {"id": pid, "title": title, "status": "created", "page": row_to_dict(row)}
|
data = {"id": pid, "title": title, "status": "created", "page": row_to_dict(row)}
|
||||||
@@ -781,13 +738,9 @@ def get_page_v2(page_id: int, request: Request, authorization: str | None = Head
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
@router.patch("/pages/{page_id}")
|
@router.patch("/pages/{page_id}")
|
||||||
async def patch_page_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def patch_page_v2(page_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
row = conn.execute("SELECT * FROM collection_pages WHERE id=?", (page_id,)).fetchone()
|
row = conn.execute("SELECT * FROM collection_pages WHERE id=?", (page_id,)).fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
@@ -822,13 +775,13 @@ async def patch_page_v2(page_id: int, request: Request, authorization: str | Non
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
audit_log(user, "page.update", "collection_page", page_id, "", request)
|
audit_log(user, "page.update", "collection_page", page_id, "", request)
|
||||||
try:
|
try:
|
||||||
await _fire_event("collection.page.updated", {"page_id": page_id, "collection_id": row["collection_id"], "title": title})
|
run_event_sync(_fire_event("collection.page.updated", {"page_id": page_id, "collection_id": row["collection_id"], "title": title}))
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("patch_page_v2")
|
logger.exception("patch_page_v2")
|
||||||
return {"id": page_id, "status": "updated"}
|
return {"id": page_id, "status": "updated"}
|
||||||
|
|
||||||
@router.delete("/pages/{page_id}")
|
@router.delete("/pages/{page_id}")
|
||||||
async def delete_page_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def delete_page_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
@@ -839,13 +792,13 @@ async def delete_page_v2(page_id: int, request: Request, authorization: str | No
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
audit_log(user, "page.delete", "collection_page", page_id, "", request)
|
audit_log(user, "page.delete", "collection_page", page_id, "", request)
|
||||||
try:
|
try:
|
||||||
await _fire_event("collection.page.deleted", {"page_id": page_id, "collection_id": row["collection_id"]})
|
run_event_sync(_fire_event("collection.page.deleted", {"page_id": page_id, "collection_id": row["collection_id"]}))
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("delete_page_v2")
|
logger.exception("delete_page_v2")
|
||||||
return {"id": page_id, "status": "deleted"}
|
return {"id": page_id, "status": "deleted"}
|
||||||
|
|
||||||
@router.post("/pages/{page_id}/restore")
|
@router.post("/pages/{page_id}/restore")
|
||||||
async def restore_page_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def restore_page_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
# For soft-deleted pages (deleted_at) - but collection_pages has no deleted_at; handle pages table
|
# For soft-deleted pages (deleted_at) - but collection_pages has no deleted_at; handle pages table
|
||||||
@@ -855,20 +808,16 @@ async def restore_page_v2(page_id: int, request: Request, authorization: str | N
|
|||||||
conn.execute("UPDATE pages SET deleted_at=NULL WHERE id=?", (page_id,))
|
conn.execute("UPDATE pages SET deleted_at=NULL WHERE id=?", (page_id,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
try:
|
try:
|
||||||
await _fire_event("page.restored", {"page_id": page_id})
|
run_event_sync(_fire_event("page.restored", {"page_id": page_id}))
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("restore_page_v2")
|
logger.exception("restore_page_v2")
|
||||||
return {"id": page_id, "status": "restored"}
|
return {"id": page_id, "status": "restored"}
|
||||||
raise HTTPException(404, "Page not found or not deleted")
|
raise HTTPException(404, "Page not found or not deleted")
|
||||||
|
|
||||||
@router.post("/pages/{page_id}/move")
|
@router.post("/pages/{page_id}/move")
|
||||||
async def move_page_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def move_page_v2(page_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
row = conn.execute("SELECT * FROM collection_pages WHERE id=?", (page_id,)).fetchone()
|
row = conn.execute("SELECT * FROM collection_pages WHERE id=?", (page_id,)).fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
@@ -891,13 +840,9 @@ def list_sub_items_v2(page_id: int, request: Request, authorization: str | None
|
|||||||
return {"sub_items": [row_to_dict(r) for r in rows]}
|
return {"sub_items": [row_to_dict(r) for r in rows]}
|
||||||
|
|
||||||
@router.post("/pages/{page_id}/sub-items")
|
@router.post("/pages/{page_id}/sub-items")
|
||||||
async def create_sub_item_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def create_sub_item_v2(page_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
parent = conn.execute("SELECT collection_id FROM collection_pages WHERE id=?", (page_id,)).fetchone()
|
parent = conn.execute("SELECT collection_id FROM collection_pages WHERE id=?", (page_id,)).fetchone()
|
||||||
if not parent:
|
if not parent:
|
||||||
@@ -920,13 +865,9 @@ def list_dependencies_v2(page_id: int, request: Request, authorization: str | No
|
|||||||
return {"dependencies": [dict(r) for r in rows]}
|
return {"dependencies": [dict(r) for r in rows]}
|
||||||
|
|
||||||
@router.post("/pages/{page_id}/dependencies")
|
@router.post("/pages/{page_id}/dependencies")
|
||||||
async def add_dependency_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def add_dependency_v2(page_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
dep_id = body.get("dependency_id") or body.get("depends_on")
|
dep_id = body.get("dependency_id") or body.get("depends_on")
|
||||||
dtype = body.get("dependency_type") or "blocks"
|
dtype = body.get("dependency_type") or "blocks"
|
||||||
if not dep_id:
|
if not dep_id:
|
||||||
@@ -967,13 +908,9 @@ def list_properties_v2(collection_id: int, request: Request, authorization: str
|
|||||||
return {"properties": [row_to_dict(r) for r in rows]}
|
return {"properties": [row_to_dict(r) for r in rows]}
|
||||||
|
|
||||||
@router.post("/collections/{collection_id}/properties")
|
@router.post("/collections/{collection_id}/properties")
|
||||||
async def create_property_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def create_property_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
name = (body.get("name") or "").strip()
|
name = (body.get("name") or "").strip()
|
||||||
if not name:
|
if not name:
|
||||||
raise HTTPException(400, "name is required")
|
raise HTTPException(400, "name is required")
|
||||||
@@ -992,13 +929,9 @@ async def create_property_v2(collection_id: int, request: Request, authorization
|
|||||||
return {"id": pid, "name": name, "prop_type": ptype, "status": "created"}
|
return {"id": pid, "name": name, "prop_type": ptype, "status": "created"}
|
||||||
|
|
||||||
@router.patch("/properties/{prop_id}")
|
@router.patch("/properties/{prop_id}")
|
||||||
async def patch_property_v2(prop_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def patch_property_v2(prop_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
row = conn.execute("SELECT * FROM collection_properties WHERE id=?", (prop_id,)).fetchone()
|
row = conn.execute("SELECT * FROM collection_properties WHERE id=?", (prop_id,)).fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
@@ -1028,13 +961,9 @@ def delete_property_v2(prop_id: int, request: Request, authorization: str | None
|
|||||||
return {"id": prop_id, "status": "deleted"}
|
return {"id": prop_id, "status": "deleted"}
|
||||||
|
|
||||||
@router.post("/properties/{prop_id}/relation")
|
@router.post("/properties/{prop_id}/relation")
|
||||||
async def create_relation_v2(prop_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def create_relation_v2(prop_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
related_id = body.get("related_collection_id")
|
related_id = body.get("related_collection_id")
|
||||||
reverse = (body.get("reverse_name") or "").strip()
|
reverse = (body.get("reverse_name") or "").strip()
|
||||||
if not related_id:
|
if not related_id:
|
||||||
@@ -1054,13 +983,9 @@ async def create_relation_v2(prop_id: int, request: Request, authorization: str
|
|||||||
return {"id": prop_id, "status": "updated"}
|
return {"id": prop_id, "status": "updated"}
|
||||||
|
|
||||||
@router.post("/properties/evaluate-formula")
|
@router.post("/properties/evaluate-formula")
|
||||||
async def evaluate_formula_v2(request: Request, authorization: str | None = Header(default=None)):
|
def evaluate_formula_v2(request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = get_bearer_user(request, authorization)
|
user = get_bearer_user(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
expr = body.get("expression") or body.get("formula")
|
expr = body.get("expression") or body.get("formula")
|
||||||
if not expr:
|
if not expr:
|
||||||
raise HTTPException(400, "expression required")
|
raise HTTPException(400, "expression required")
|
||||||
@@ -1073,13 +998,9 @@ async def evaluate_formula_v2(request: Request, authorization: str | None = Head
|
|||||||
return {"result": res, "expression": expr}
|
return {"result": res, "expression": expr}
|
||||||
|
|
||||||
@router.post("/properties/compute-rollup")
|
@router.post("/properties/compute-rollup")
|
||||||
async def compute_rollup_v2(request: Request, authorization: str | None = Header(default=None)):
|
def compute_rollup_v2(request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = get_bearer_user(request, authorization)
|
user = get_bearer_user(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
for k in ("collection_id", "relation_property_id", "target_property_id", "page_id"):
|
for k in ("collection_id", "relation_property_id", "target_property_id", "page_id"):
|
||||||
if k not in body:
|
if k not in body:
|
||||||
raise HTTPException(400, f"{k} required")
|
raise HTTPException(400, f"{k} required")
|
||||||
@@ -1101,13 +1022,9 @@ def list_views_v2(collection_id: int, request: Request, authorization: str | Non
|
|||||||
return {"views": [row_to_dict(r) for r in rows]}
|
return {"views": [row_to_dict(r) for r in rows]}
|
||||||
|
|
||||||
@router.post("/collections/{collection_id}/views")
|
@router.post("/collections/{collection_id}/views")
|
||||||
async def create_view_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def create_view_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
name = (body.get("name") or "New View").strip()
|
name = (body.get("name") or "New View").strip()
|
||||||
vtype = body.get("view_type") or body.get("type") or "table"
|
vtype = body.get("view_type") or body.get("type") or "table"
|
||||||
config = body.get("config") or body.get("config_json") or {}
|
config = body.get("config") or body.get("config_json") or {}
|
||||||
@@ -1120,19 +1037,15 @@ async def create_view_v2(collection_id: int, request: Request, authorization: st
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
audit_log(user, "view.create", "view", vid, name, request)
|
audit_log(user, "view.create", "view", vid, name, request)
|
||||||
try:
|
try:
|
||||||
await _fire_event("collection.view.created", {"view_id": vid, "collection_id": collection_id, "name": name, "view_type": vtype})
|
run_event_sync(_fire_event("collection.view.created", {"view_id": vid, "collection_id": collection_id, "name": name, "view_type": vtype}))
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("create_view_v2")
|
logger.exception("create_view_v2")
|
||||||
return {"id": vid, "name": name, "view_type": vtype, "status": "created"}
|
return {"id": vid, "name": name, "view_type": vtype, "status": "created"}
|
||||||
|
|
||||||
@router.patch("/views/{view_id}")
|
@router.patch("/views/{view_id}")
|
||||||
async def patch_view_v2(view_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def patch_view_v2(view_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
row = conn.execute("SELECT * FROM collection_views WHERE id=?", (view_id,)).fetchone()
|
row = conn.execute("SELECT * FROM collection_views WHERE id=?", (view_id,)).fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
@@ -1169,13 +1082,9 @@ def delete_view_v2(view_id: int, request: Request, authorization: str | None = H
|
|||||||
return {"id": view_id, "status": "deleted"}
|
return {"id": view_id, "status": "deleted"}
|
||||||
|
|
||||||
@router.post("/views/{view_id}/save-as")
|
@router.post("/views/{view_id}/save-as")
|
||||||
async def save_as_view_v2(view_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def save_as_view_v2(view_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
name = (body.get("name") or "").strip() or "Copy"
|
name = (body.get("name") or "").strip() or "Copy"
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
row = conn.execute("SELECT * FROM collection_views WHERE id=?", (view_id,)).fetchone()
|
row = conn.execute("SELECT * FROM collection_views WHERE id=?", (view_id,)).fetchone()
|
||||||
@@ -1196,13 +1105,9 @@ def list_dashboards_v2(collection_id: int, request: Request, authorization: str
|
|||||||
return {"dashboards": [row_to_dict(r) for r in rows]}
|
return {"dashboards": [row_to_dict(r) for r in rows]}
|
||||||
|
|
||||||
@router.post("/collections/{collection_id}/dashboards")
|
@router.post("/collections/{collection_id}/dashboards")
|
||||||
async def create_dashboard_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def create_dashboard_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
name = (body.get("name") or "Dashboard").strip()
|
name = (body.get("name") or "Dashboard").strip()
|
||||||
layout = body.get("layout") or body.get("layout_json") or {"columns": 1, "widgets": []}
|
layout = body.get("layout") or body.get("layout_json") or {"columns": 1, "widgets": []}
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
@@ -1212,13 +1117,9 @@ async def create_dashboard_v2(collection_id: int, request: Request, authorizatio
|
|||||||
return {"id": did, "name": name, "status": "created"}
|
return {"id": did, "name": name, "status": "created"}
|
||||||
|
|
||||||
@router.patch("/dashboards/{dashboard_id}")
|
@router.patch("/dashboards/{dashboard_id}")
|
||||||
async def patch_dashboard_v2(dashboard_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def patch_dashboard_v2(dashboard_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
row = conn.execute("SELECT * FROM collection_dashboards WHERE id=?", (dashboard_id,)).fetchone()
|
row = conn.execute("SELECT * FROM collection_dashboards WHERE id=?", (dashboard_id,)).fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
@@ -1255,13 +1156,9 @@ def list_comments_v2(page_id: int, request: Request, authorization: str | None =
|
|||||||
return {"comments": [row_to_dict(r) for r in rows], "total": total, "limit": limit, "offset": offset}
|
return {"comments": [row_to_dict(r) for r in rows], "total": total, "limit": limit, "offset": offset}
|
||||||
|
|
||||||
@router.post("/pages/{page_id}/comments")
|
@router.post("/pages/{page_id}/comments")
|
||||||
async def create_comment_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def create_comment_v2(page_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
text = (body.get("body") or body.get("content") or "").strip()
|
text = (body.get("body") or body.get("content") or "").strip()
|
||||||
if not text:
|
if not text:
|
||||||
raise HTTPException(400, "body is required")
|
raise HTTPException(400, "body is required")
|
||||||
@@ -1272,19 +1169,15 @@ async def create_comment_v2(page_id: int, request: Request, authorization: str |
|
|||||||
row = conn.execute("SELECT * FROM comments WHERE id=?", (nid,)).fetchone()
|
row = conn.execute("SELECT * FROM comments WHERE id=?", (nid,)).fetchone()
|
||||||
audit_log(user, "comment.create", "comment", nid, text[:80], request)
|
audit_log(user, "comment.create", "comment", nid, text[:80], request)
|
||||||
try:
|
try:
|
||||||
await _fire_event("comment.added", {"comment_id": nid, "page_id": page_id, "user_id": user["id"]})
|
run_event_sync(_fire_event("comment.added", {"comment_id": nid, "page_id": page_id, "user_id": user["id"]}))
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("create_comment_v2")
|
logger.exception("create_comment_v2")
|
||||||
return {"id": nid, "status": "created", "comment": row_to_dict(row)}
|
return {"id": nid, "status": "created", "comment": row_to_dict(row)}
|
||||||
|
|
||||||
@router.patch("/comments/{comment_id}")
|
@router.patch("/comments/{comment_id}")
|
||||||
async def patch_comment_v2(comment_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def patch_comment_v2(comment_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
row = conn.execute("SELECT * FROM comments WHERE id=?", (comment_id,)).fetchone()
|
row = conn.execute("SELECT * FROM comments WHERE id=?", (comment_id,)).fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
@@ -1298,7 +1191,7 @@ async def patch_comment_v2(comment_id: int, request: Request, authorization: str
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
if int(bool(resolved)) and not was_resolved:
|
if int(bool(resolved)) and not was_resolved:
|
||||||
try:
|
try:
|
||||||
await _fire_event("comment.resolved", {"comment_id": comment_id, "page_id": row["page_id"]})
|
run_event_sync(_fire_event("comment.resolved", {"comment_id": comment_id, "page_id": row["page_id"]}))
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("patch_comment_v2")
|
logger.exception("patch_comment_v2")
|
||||||
return {"id": comment_id, "status": "updated"}
|
return {"id": comment_id, "status": "updated"}
|
||||||
@@ -1318,13 +1211,9 @@ def delete_comment_v2(comment_id: int, request: Request, authorization: str | No
|
|||||||
return {"id": comment_id, "status": "deleted"}
|
return {"id": comment_id, "status": "deleted"}
|
||||||
|
|
||||||
@router.post("/pages/{page_id}/mentions")
|
@router.post("/pages/{page_id}/mentions")
|
||||||
async def create_mention_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def create_mention_v2(page_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
targets = body.get("user_ids") or body.get("mentions") or []
|
targets = body.get("user_ids") or body.get("mentions") or []
|
||||||
if isinstance(targets, int):
|
if isinstance(targets, int):
|
||||||
targets = [targets]
|
targets = [targets]
|
||||||
@@ -1341,7 +1230,7 @@ async def create_mention_v2(page_id: int, request: Request, authorization: str |
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
if created:
|
if created:
|
||||||
try:
|
try:
|
||||||
await _fire_event("mention.added", {"page_id": page_id, "user_ids": [u for u in targets if isinstance(u, int)], "count": created})
|
run_event_sync(_fire_event("mention.added", {"page_id": page_id, "user_ids": [u for u in targets if isinstance(u, int)], "count": created}))
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("create_mention_v2")
|
logger.exception("create_mention_v2")
|
||||||
return {"mentions": created, "status": "created"}
|
return {"mentions": created, "status": "created"}
|
||||||
@@ -1390,13 +1279,9 @@ def mark_all_read(request: Request, authorization: str | None = Header(default=N
|
|||||||
return {"status": "all read"}
|
return {"status": "all read"}
|
||||||
|
|
||||||
@router.patch("/users/me/preferences")
|
@router.patch("/users/me/preferences")
|
||||||
async def patch_prefs(request: Request, authorization: str | None = Header(default=None)):
|
def patch_prefs(request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = get_bearer_user(request, authorization)
|
user = get_bearer_user(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
row = conn.execute("SELECT notification_prefs FROM users WHERE id=?", (user["id"],)).fetchone()
|
row = conn.execute("SELECT notification_prefs FROM users WHERE id=?", (user["id"],)).fetchone()
|
||||||
try:
|
try:
|
||||||
@@ -1419,13 +1304,9 @@ def list_favorites_v2(request: Request, authorization: str | None = Header(defau
|
|||||||
return {"favorites": [row_to_dict(r) for r in rows]}
|
return {"favorites": [row_to_dict(r) for r in rows]}
|
||||||
|
|
||||||
@router.post("/favorites")
|
@router.post("/favorites")
|
||||||
async def add_favorite_v2(request: Request, authorization: str | None = Header(default=None)):
|
def add_favorite_v2(request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
pid = body.get("page_id")
|
pid = body.get("page_id")
|
||||||
if not pid:
|
if not pid:
|
||||||
raise HTTPException(400, "page_id required")
|
raise HTTPException(400, "page_id required")
|
||||||
@@ -1436,20 +1317,20 @@ async def add_favorite_v2(request: Request, authorization: str | None = Header(d
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(409, str(e)) from None
|
raise HTTPException(409, str(e)) from None
|
||||||
try:
|
try:
|
||||||
await _fire_event("favorite.added", {"page_id": pid, "user_id": user["id"]})
|
run_event_sync(_fire_event("favorite.added", {"page_id": pid, "user_id": user["id"]}))
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("add_favorite_v2")
|
logger.exception("add_favorite_v2")
|
||||||
return {"page_id": pid, "status": "added"}
|
return {"page_id": pid, "status": "added"}
|
||||||
|
|
||||||
@router.delete("/favorites/{page_id}")
|
@router.delete("/favorites/{page_id}")
|
||||||
async def remove_favorite_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def remove_favorite_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
conn.execute("DELETE FROM favorites WHERE user_id=? AND page_id=?", (user["id"], page_id))
|
conn.execute("DELETE FROM favorites WHERE user_id=? AND page_id=?", (user["id"], page_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
try:
|
try:
|
||||||
await _fire_event("favorite.removed", {"page_id": page_id, "user_id": user["id"]})
|
run_event_sync(_fire_event("favorite.removed", {"page_id": page_id, "user_id": user["id"]}))
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("remove_favorite_v2")
|
logger.exception("remove_favorite_v2")
|
||||||
return {"page_id": page_id, "status": "removed"}
|
return {"page_id": page_id, "status": "removed"}
|
||||||
@@ -1467,13 +1348,9 @@ def list_tags_v2(request: Request, authorization: str | None = Header(default=No
|
|||||||
return {"tags": [dict(r) for r in rows]}
|
return {"tags": [dict(r) for r in rows]}
|
||||||
|
|
||||||
@router.post("/tags")
|
@router.post("/tags")
|
||||||
async def create_tag_v2(request: Request, authorization: str | None = Header(default=None)):
|
def create_tag_v2(request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
name = (body.get("name") or "").strip()
|
name = (body.get("name") or "").strip()
|
||||||
if not name:
|
if not name:
|
||||||
raise HTTPException(400, "name required")
|
raise HTTPException(400, "name required")
|
||||||
@@ -1488,13 +1365,9 @@ async def create_tag_v2(request: Request, authorization: str | None = Header(def
|
|||||||
return {"id": tid, "name": name, "color": color, "status": "created"}
|
return {"id": tid, "name": name, "color": color, "status": "created"}
|
||||||
|
|
||||||
@router.patch("/tags/{tag_id}")
|
@router.patch("/tags/{tag_id}")
|
||||||
async def patch_tag_v2(tag_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def patch_tag_v2(tag_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
row = conn.execute("SELECT * FROM tags WHERE id=? AND user_id=?", (tag_id, user["id"])).fetchone()
|
row = conn.execute("SELECT * FROM tags WHERE id=? AND user_id=?", (tag_id, user["id"])).fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
@@ -1515,13 +1388,9 @@ def delete_tag_v2(tag_id: int, request: Request, authorization: str | None = Hea
|
|||||||
return {"id": tag_id, "status": "deleted"}
|
return {"id": tag_id, "status": "deleted"}
|
||||||
|
|
||||||
@router.post("/pages/{page_id}/tags")
|
@router.post("/pages/{page_id}/tags")
|
||||||
async def attach_tag_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def attach_tag_v2(page_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
tag_id = body.get("tag_id")
|
tag_id = body.get("tag_id")
|
||||||
if not tag_id:
|
if not tag_id:
|
||||||
raise HTTPException(400, "tag_id required")
|
raise HTTPException(400, "tag_id required")
|
||||||
@@ -1566,13 +1435,9 @@ def list_shares_v2(page_id: int, request: Request, authorization: str | None = H
|
|||||||
return {"shares": [dict(r) for r in rows]}
|
return {"shares": [dict(r) for r in rows]}
|
||||||
|
|
||||||
@router.post("/pages/{page_id}/shares")
|
@router.post("/pages/{page_id}/shares")
|
||||||
async def create_share_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def create_share_v2(page_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
perm = (body.get("permission") or "view").strip().lower()
|
perm = (body.get("permission") or "view").strip().lower()
|
||||||
if perm not in ("view", "comment", "edit"):
|
if perm not in ("view", "comment", "edit"):
|
||||||
raise HTTPException(400, "Invalid permission. Use view, comment, or edit")
|
raise HTTPException(400, "Invalid permission. Use view, comment, or edit")
|
||||||
@@ -1587,19 +1452,15 @@ async def create_share_v2(page_id: int, request: Request, authorization: str | N
|
|||||||
nid = cur.lastrowid
|
nid = cur.lastrowid
|
||||||
audit_log(user, "share.create", "share", nid, f"page={page_id}", request)
|
audit_log(user, "share.create", "share", nid, f"page={page_id}", request)
|
||||||
try:
|
try:
|
||||||
await _fire_event("page.shared", {"page_id": page_id, "share_id": nid, "permission": perm})
|
run_event_sync(_fire_event("page.shared", {"page_id": page_id, "share_id": nid, "permission": perm}))
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("create_share_v2")
|
logger.exception("create_share_v2")
|
||||||
return {"id": nid, "page_id": page_id, "status": "shared"}
|
return {"id": nid, "page_id": page_id, "status": "shared"}
|
||||||
|
|
||||||
@router.patch("/shares/{share_id}")
|
@router.patch("/shares/{share_id}")
|
||||||
async def patch_share_v2(share_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def patch_share_v2(share_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
perm = (body.get("permission") or "").strip().lower()
|
perm = (body.get("permission") or "").strip().lower()
|
||||||
if perm not in ("view", "comment", "edit"):
|
if perm not in ("view", "comment", "edit"):
|
||||||
raise HTTPException(400, "Invalid permission")
|
raise HTTPException(400, "Invalid permission")
|
||||||
@@ -1627,25 +1488,21 @@ def delete_share_v2(share_id: int, request: Request, authorization: str | None =
|
|||||||
return {"id": share_id, "status": "revoked"}
|
return {"id": share_id, "status": "revoked"}
|
||||||
|
|
||||||
@router.post("/pages/{page_id}/publish")
|
@router.post("/pages/{page_id}/publish")
|
||||||
async def publish_page_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def publish_page_v2(page_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
slug_in = (body.get("slug") or body.get("publish_slug") or "").strip() or None
|
slug_in = (body.get("slug") or body.get("publish_slug") or "").strip() or None
|
||||||
slug, _title = publish(page_id, explicit_slug=slug_in)
|
slug, _title = publish(page_id, explicit_slug=slug_in)
|
||||||
audit_log(user, "page.publish", "page", page_id, slug, request)
|
audit_log(user, "page.publish", "page", page_id, slug, request)
|
||||||
await fire_published(page_id, slug)
|
run_event_sync(fire_published(page_id, slug))
|
||||||
return {"page_id": page_id, "slug": slug, "url": f"/p/{slug}", "status": "published"}
|
return {"page_id": page_id, "slug": slug, "url": f"/p/{slug}", "status": "published"}
|
||||||
|
|
||||||
@router.delete("/pages/{page_id}/publish")
|
@router.delete("/pages/{page_id}/publish")
|
||||||
async def unpublish_page_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def unpublish_page_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
unpublish(page_id)
|
unpublish(page_id)
|
||||||
await fire_unpublished(page_id)
|
run_event_sync(fire_unpublished(page_id))
|
||||||
return {"page_id": page_id, "status": "unpublished"}
|
return {"page_id": page_id, "status": "unpublished"}
|
||||||
|
|
||||||
# ── History ───────────────────────────────────────────────────────────────
|
# ── History ───────────────────────────────────────────────────────────────
|
||||||
@@ -1661,13 +1518,9 @@ def list_history_v2(page_id: int, request: Request, authorization: str | None =
|
|||||||
return {"history": [row_to_dict(r) for r in rows], "versions": [row_to_dict(r) for r in vrows]}
|
return {"history": [row_to_dict(r) for r in rows], "versions": [row_to_dict(r) for r in vrows]}
|
||||||
|
|
||||||
@router.post("/pages/{page_id}/history/restore")
|
@router.post("/pages/{page_id}/history/restore")
|
||||||
async def restore_history_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def restore_history_v2(page_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
hid = body.get("history_id") or body.get("id") or body.get("version_id")
|
hid = body.get("history_id") or body.get("id") or body.get("version_id")
|
||||||
if not hid:
|
if not hid:
|
||||||
raise HTTPException(400, "history_id required")
|
raise HTTPException(400, "history_id required")
|
||||||
@@ -1703,13 +1556,9 @@ def list_sprints_v2(collection_id: int, request: Request, authorization: str | N
|
|||||||
return {"sprints": [row_to_dict(r) for r in rows], "total": total, "limit": limit, "offset": offset}
|
return {"sprints": [row_to_dict(r) for r in rows], "total": total, "limit": limit, "offset": offset}
|
||||||
|
|
||||||
@router.post("/collections/{collection_id}/sprints")
|
@router.post("/collections/{collection_id}/sprints")
|
||||||
async def create_sprint_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def create_sprint_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
name = (body.get("name") or "Sprint").strip()
|
name = (body.get("name") or "Sprint").strip()
|
||||||
start = body.get("start_date") or body.get("start") or ""
|
start = body.get("start_date") or body.get("start") or ""
|
||||||
end = body.get("end_date") or body.get("end") or ""
|
end = body.get("end_date") or body.get("end") or ""
|
||||||
@@ -1720,19 +1569,15 @@ async def create_sprint_v2(collection_id: int, request: Request, authorization:
|
|||||||
sid = cur.lastrowid
|
sid = cur.lastrowid
|
||||||
conn.commit()
|
conn.commit()
|
||||||
try:
|
try:
|
||||||
await _fire_event("sprint.created", {"sprint_id": sid, "collection_id": collection_id, "name": name})
|
run_event_sync(_fire_event("sprint.created", {"sprint_id": sid, "collection_id": collection_id, "name": name}))
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("create_sprint_v2")
|
logger.exception("create_sprint_v2")
|
||||||
return {"id": sid, "name": name, "status": "created"}
|
return {"id": sid, "name": name, "status": "created"}
|
||||||
|
|
||||||
@router.patch("/sprints/{sprint_id}")
|
@router.patch("/sprints/{sprint_id}")
|
||||||
async def patch_sprint_v2(sprint_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def patch_sprint_v2(sprint_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
row = conn.execute("SELECT * FROM sprints WHERE id=?", (sprint_id,)).fetchone()
|
row = conn.execute("SELECT * FROM sprints WHERE id=?", (sprint_id,)).fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
@@ -1745,7 +1590,7 @@ async def patch_sprint_v2(sprint_id: int, request: Request, authorization: str |
|
|||||||
conn.execute("UPDATE sprints SET name=?, start_date=?, end_date=?, goal=?, status=? WHERE id=?", (name, start, end, goal, status, sprint_id))
|
conn.execute("UPDATE sprints SET name=?, start_date=?, end_date=?, goal=?, status=? WHERE id=?", (name, start, end, goal, status, sprint_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
try:
|
try:
|
||||||
await _fire_event("sprint.updated", {"sprint_id": sprint_id, "collection_id": row["collection_id"], "name": name, "status": status})
|
run_event_sync(_fire_event("sprint.updated", {"sprint_id": sprint_id, "collection_id": row["collection_id"], "name": name, "status": status}))
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("patch_sprint_v2")
|
logger.exception("patch_sprint_v2")
|
||||||
return {"id": sprint_id, "status": "updated"}
|
return {"id": sprint_id, "status": "updated"}
|
||||||
@@ -1760,13 +1605,9 @@ def delete_sprint_v2(sprint_id: int, request: Request, authorization: str | None
|
|||||||
return {"id": sprint_id, "status": "deleted"}
|
return {"id": sprint_id, "status": "deleted"}
|
||||||
|
|
||||||
@router.post("/sprints/{sprint_id}/assign")
|
@router.post("/sprints/{sprint_id}/assign")
|
||||||
async def assign_sprint_v2(sprint_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def assign_sprint_v2(sprint_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
pid = body.get("page_id")
|
pid = body.get("page_id")
|
||||||
if not pid:
|
if not pid:
|
||||||
raise HTTPException(400, "page_id required")
|
raise HTTPException(400, "page_id required")
|
||||||
@@ -1824,13 +1665,9 @@ def list_templates_v2(collection_id: int, request: Request, authorization: str |
|
|||||||
return {"templates": [row_to_dict(r) for r in rows]}
|
return {"templates": [row_to_dict(r) for r in rows]}
|
||||||
|
|
||||||
@router.post("/collections/{collection_id}/templates")
|
@router.post("/collections/{collection_id}/templates")
|
||||||
async def create_template_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def create_template_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
name = (body.get("name") or "Template").strip()
|
name = (body.get("name") or "Template").strip()
|
||||||
pv = json.dumps(body.get("property_values") or body.get("property_values_json") or {})
|
pv = json.dumps(body.get("property_values") or body.get("property_values_json") or {})
|
||||||
cj = json.dumps(body.get("content") or body.get("content_json") or [])
|
cj = json.dumps(body.get("content") or body.get("content_json") or [])
|
||||||
@@ -1841,13 +1678,9 @@ async def create_template_v2(collection_id: int, request: Request, authorization
|
|||||||
return {"id": tid, "name": name, "status": "created"}
|
return {"id": tid, "name": name, "status": "created"}
|
||||||
|
|
||||||
@router.patch("/templates/{template_id}")
|
@router.patch("/templates/{template_id}")
|
||||||
async def patch_template_v2(template_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def patch_template_v2(template_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
row = conn.execute("SELECT * FROM page_templates WHERE id=?", (template_id,)).fetchone()
|
row = conn.execute("SELECT * FROM page_templates WHERE id=?", (template_id,)).fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
@@ -1892,13 +1725,9 @@ def list_db_templates_v2(request: Request, authorization: str | None = Header(de
|
|||||||
return {"templates": [row_to_dict(r) for r in rows]}
|
return {"templates": [row_to_dict(r) for r in rows]}
|
||||||
|
|
||||||
@router.post("/templates/database/{template_id}/apply")
|
@router.post("/templates/database/{template_id}/apply")
|
||||||
async def apply_db_template_v2(template_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def apply_db_template_v2(template_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
tpl = conn.execute("SELECT * FROM database_templates WHERE id=?", (template_id,)).fetchone()
|
tpl = conn.execute("SELECT * FROM database_templates WHERE id=?", (template_id,)).fetchone()
|
||||||
if not tpl:
|
if not tpl:
|
||||||
@@ -2065,15 +1894,11 @@ def admin_list_users_v2(request: Request, limit: int = 30, offset: int = 0, auth
|
|||||||
return {"users": [row_to_dict(r) for r in rows], "total": total, "limit": limit, "offset": offset}
|
return {"users": [row_to_dict(r) for r in rows], "total": total, "limit": limit, "offset": offset}
|
||||||
|
|
||||||
@router.patch("/admin/users/{uid}")
|
@router.patch("/admin/users/{uid}")
|
||||||
async def admin_patch_user_v2(uid: int, request: Request, authorization: str | None = Header(default=None)):
|
def admin_patch_user_v2(uid: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = get_bearer_user(request, authorization)
|
user = get_bearer_user(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
if not user.get("is_admin") and not has_scope(user.get("_token_scopes"), "admin"):
|
if not user.get("is_admin") and not has_scope(user.get("_token_scopes"), "admin"):
|
||||||
raise HTTPException(403, "Admin scope required")
|
raise HTTPException(403, "Admin scope required")
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
if not conn.execute("SELECT id FROM users WHERE id=?", (uid,)).fetchone():
|
if not conn.execute("SELECT id FROM users WHERE id=?", (uid,)).fetchone():
|
||||||
raise HTTPException(404, "User not found")
|
raise HTTPException(404, "User not found")
|
||||||
@@ -2144,13 +1969,9 @@ def list_webhooks_v2(request: Request, authorization: str | None = Header(defaul
|
|||||||
)
|
)
|
||||||
|
|
||||||
@router.post("/webhooks")
|
@router.post("/webhooks")
|
||||||
async def create_webhook_v2(request: Request, authorization: str | None = Header(default=None)):
|
def create_webhook_v2(request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
from app.services.webhook_outbound import EVENTS, _event_matches
|
from app.services.webhook_outbound import EVENTS, _event_matches
|
||||||
url = (body.get("url") or "").strip()
|
url = (body.get("url") or "").strip()
|
||||||
event = (body.get("event") or "page.created").strip()
|
event = (body.get("event") or "page.created").strip()
|
||||||
@@ -2172,13 +1993,9 @@ async def create_webhook_v2(request: Request, authorization: str | None = Header
|
|||||||
"signature_header": "X-FlowDeck-Signature (HMAC-SHA256, sha256=<hex>)" if secret else None}
|
"signature_header": "X-FlowDeck-Signature (HMAC-SHA256, sha256=<hex>)" if secret else None}
|
||||||
|
|
||||||
@router.patch("/webhooks/{webhook_id}")
|
@router.patch("/webhooks/{webhook_id}")
|
||||||
async def patch_webhook_v2(webhook_id: int, request: Request, authorization: str | None = Header(default=None)):
|
def patch_webhook_v2(webhook_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
|
||||||
user = require_scope("write")(request, authorization)
|
user = require_scope("write")(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
row = conn.execute("SELECT * FROM webhook_subscriptions WHERE id=?", (webhook_id,)).fetchone()
|
row = conn.execute("SELECT * FROM webhook_subscriptions WHERE id=?", (webhook_id,)).fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
@@ -2274,19 +2091,15 @@ async def retry_webhook_deliveries(webhook_id: int, request: Request,
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/webhooks/verify-signature")
|
@router.post("/webhooks/verify-signature")
|
||||||
async def verify_webhook_signature(request: Request,
|
def verify_webhook_signature(request: Request,
|
||||||
authorization: str | None = Header(default=None)):
|
authorization: str | None = Header(default=None),
|
||||||
|
body: dict = Body(default={})):
|
||||||
"""Verify a webhook signature (for debugging/testing)."""
|
"""Verify a webhook signature (for debugging/testing)."""
|
||||||
user = get_bearer_user(request, authorization)
|
user = get_bearer_user(request, authorization)
|
||||||
_v2_rate_check(request, user)
|
_v2_rate_check(request, user)
|
||||||
|
|
||||||
from app.services.webhook_outbound import verify_signature
|
from app.services.webhook_outbound import verify_signature
|
||||||
|
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except Exception:
|
|
||||||
body = {}
|
|
||||||
|
|
||||||
secret = body.get("secret", "")
|
secret = body.get("secret", "")
|
||||||
payload = body.get("payload", "{}")
|
payload = body.get("payload", "{}")
|
||||||
signature = body.get("signature", "")
|
signature = body.get("signature", "")
|
||||||
|
|||||||
@@ -341,74 +341,6 @@ def library_private(
|
|||||||
return {"items": items}
|
return {"items": items}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/local-workspace-children/{item_id:int}")
|
|
||||||
def library_local_workspace_children(item_id: int, request: Request):
|
|
||||||
"""Return children of a local workspace item for tree expansion."""
|
|
||||||
_get_user_id(request)
|
|
||||||
with get_conn() as conn:
|
|
||||||
# Get the item to find its workspace
|
|
||||||
item = conn.execute(
|
|
||||||
"SELECT workspace_id FROM local_workspace_items WHERE id=? AND deleted_at IS NULL",
|
|
||||||
[item_id],
|
|
||||||
).fetchone()
|
|
||||||
if not item:
|
|
||||||
return {"items": []}
|
|
||||||
|
|
||||||
rows = conn.execute(
|
|
||||||
"SELECT id, name, is_folder, parent_id, content_format, size, "
|
|
||||||
"COALESCE(updated_at, created_at) as updated_at "
|
|
||||||
"FROM local_workspace_items "
|
|
||||||
"WHERE parent_id = ? AND deleted_at IS NULL "
|
|
||||||
"ORDER BY is_folder DESC, name COLLATE NOCASE",
|
|
||||||
[item_id],
|
|
||||||
).fetchall()
|
|
||||||
|
|
||||||
items = []
|
|
||||||
for r in rows:
|
|
||||||
name = r["name"] or "Untitled"
|
|
||||||
is_folder = bool(r["is_folder"])
|
|
||||||
icon = "📁" if is_folder else "📄"
|
|
||||||
fn = name.lower()
|
|
||||||
if not is_folder:
|
|
||||||
if fn.endswith(".pdf"):
|
|
||||||
icon = "📕"
|
|
||||||
elif any(fn.endswith(e) for e in [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]):
|
|
||||||
icon = "🖼️"
|
|
||||||
elif any(fn.endswith(e) for e in [".py", ".js", ".ts", ".go", ".rs"]):
|
|
||||||
icon = "📜"
|
|
||||||
|
|
||||||
with get_conn() as conn:
|
|
||||||
child_count = conn.execute(
|
|
||||||
"SELECT COUNT(*) FROM local_workspace_items WHERE parent_id=? AND deleted_at IS NULL",
|
|
||||||
[r["id"]],
|
|
||||||
).fetchone()[0]
|
|
||||||
|
|
||||||
items.append({
|
|
||||||
"id": r["id"],
|
|
||||||
"title": name,
|
|
||||||
"icon": icon,
|
|
||||||
"is_folder": is_folder,
|
|
||||||
"source_type": "local-ws",
|
|
||||||
"source_label": "",
|
|
||||||
"workspace": "",
|
|
||||||
"workspace_name": "",
|
|
||||||
"author": "",
|
|
||||||
"author_initial": "?",
|
|
||||||
"updated_at": r["updated_at"] or "",
|
|
||||||
"visited_at": "",
|
|
||||||
"has_children": child_count > 0,
|
|
||||||
"children": [],
|
|
||||||
"url": f"/local-workspace?folder={r['id']}" if is_folder else f"/pages/{r['id']}",
|
|
||||||
"content_format": r["content_format"] or "file",
|
|
||||||
"favorited": False,
|
|
||||||
"page_icon": "",
|
|
||||||
"tags": [],
|
|
||||||
"size_display": _format_size(r["size"]) if r["size"] else "",
|
|
||||||
})
|
|
||||||
|
|
||||||
return {"items": items}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/children/{page_id:int}")
|
@router.get("/children/{page_id:int}")
|
||||||
def library_children(page_id: int, request: Request):
|
def library_children(page_id: int, request: Request):
|
||||||
"""Return child pages for a given parent page (for tree expansion in Library)."""
|
"""Return child pages for a given parent page (for tree expansion in Library)."""
|
||||||
@@ -449,91 +381,6 @@ def library_repository(
|
|||||||
return {"items": items}
|
return {"items": items}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/local-workspace")
|
|
||||||
def library_local_workspace(
|
|
||||||
request: Request,
|
|
||||||
workspace_id: int = Query(default=0),
|
|
||||||
):
|
|
||||||
"""Return local workspace items (files/folders) formatted for Library display."""
|
|
||||||
from app.routers.dashboard import _get_active_workspace
|
|
||||||
uid = _get_user_id(request)
|
|
||||||
|
|
||||||
# Get the active workspace
|
|
||||||
ws = _get_active_workspace(request, user_id=uid)
|
|
||||||
if not ws:
|
|
||||||
return {"items": []}
|
|
||||||
|
|
||||||
ws_id = workspace_id or ws["id"]
|
|
||||||
|
|
||||||
# Query local workspace tree
|
|
||||||
with get_conn() as conn:
|
|
||||||
rows = conn.execute(
|
|
||||||
"SELECT id, name, is_folder, parent_id, content_format, size, "
|
|
||||||
"COALESCE(updated_at, created_at) as updated_at "
|
|
||||||
"FROM local_workspace_items "
|
|
||||||
"WHERE workspace_id = ? AND deleted_at IS NULL "
|
|
||||||
"ORDER BY is_folder DESC, name COLLATE NOCASE",
|
|
||||||
[ws_id],
|
|
||||||
).fetchall()
|
|
||||||
|
|
||||||
items = []
|
|
||||||
for r in rows:
|
|
||||||
name = r["name"] or "Untitled"
|
|
||||||
is_folder = bool(r["is_folder"])
|
|
||||||
icon = "📁" if is_folder else "📄"
|
|
||||||
fn = name.lower()
|
|
||||||
if not is_folder:
|
|
||||||
if fn.endswith(".pdf"):
|
|
||||||
icon = "📕"
|
|
||||||
elif any(fn.endswith(e) for e in [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]):
|
|
||||||
icon = "🖼️"
|
|
||||||
elif any(fn.endswith(e) for e in [".py", ".js", ".ts", ".go", ".rs"]):
|
|
||||||
icon = "📜"
|
|
||||||
|
|
||||||
# Check for children
|
|
||||||
child_count = conn.execute(
|
|
||||||
"SELECT COUNT(*) FROM local_workspace_items WHERE parent_id=? AND deleted_at IS NULL",
|
|
||||||
[r["id"]],
|
|
||||||
).fetchone()[0]
|
|
||||||
|
|
||||||
items.append({
|
|
||||||
"id": r["id"],
|
|
||||||
"title": name,
|
|
||||||
"icon": icon,
|
|
||||||
"is_folder": is_folder,
|
|
||||||
"source_type": "local-ws",
|
|
||||||
"source_label": ws.get("name", "Workspace"),
|
|
||||||
"workspace": ws.get("name", ""),
|
|
||||||
"workspace_name": ws.get("name", ""),
|
|
||||||
"author": "",
|
|
||||||
"author_initial": "?",
|
|
||||||
"updated_at": r["updated_at"] or "",
|
|
||||||
"visited_at": "",
|
|
||||||
"has_children": child_count > 0,
|
|
||||||
"children": [],
|
|
||||||
"url": f"/local-workspace?folder={r['id']}" if is_folder else f"/pages/{r['id']}",
|
|
||||||
"content_format": r["content_format"] or "file",
|
|
||||||
"favorited": False,
|
|
||||||
"page_icon": "",
|
|
||||||
"tags": [],
|
|
||||||
"size_display": _format_size(r["size"]) if r["size"] else "",
|
|
||||||
})
|
|
||||||
|
|
||||||
return {"items": items}
|
|
||||||
|
|
||||||
|
|
||||||
def _format_size(size_bytes):
|
|
||||||
if not size_bytes:
|
|
||||||
return ""
|
|
||||||
if size_bytes < 1024:
|
|
||||||
return f"{size_bytes} B"
|
|
||||||
if size_bytes < 1048576:
|
|
||||||
return f"{size_bytes/1024:.1f} KB"
|
|
||||||
if size_bytes < 1073741824:
|
|
||||||
return f"{size_bytes/1048576:.1f} MB"
|
|
||||||
return f"{size_bytes/1073741824:.1f} GB"
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/workspace")
|
@router.get("/workspace")
|
||||||
def library_workspace(
|
def library_workspace(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -355,6 +355,19 @@ async def run_automation(automation_id: int, trigger_source: str, context: dict)
|
|||||||
return {"status": "error", "detail": str(exc)}
|
return {"status": "error", "detail": str(exc)}
|
||||||
|
|
||||||
|
|
||||||
|
def run_event_sync(coro, timeout: float = 60.0):
|
||||||
|
"""A21 phase 2b : exécute une coroutine d'événement depuis un handler synchrone.
|
||||||
|
|
||||||
|
Bloque le worker threadpool (jamais la boucle d'event) et attend la fin —
|
||||||
|
déterministe, exactement ce que faisait l'await avant la conversion des
|
||||||
|
routes en `def`.
|
||||||
|
ponytail: les clients httpx des services sont créés à chaque appel (aucun
|
||||||
|
lien de boucle) ; si un jour un client/queue est lié à la boucle de l'app,
|
||||||
|
passer à `asyncio.run_coroutine_threadsafe` + boucle capturée au lifespan.
|
||||||
|
"""
|
||||||
|
return asyncio.run(asyncio.wait_for(coro, timeout))
|
||||||
|
|
||||||
|
|
||||||
async def fire_event(event: str, payload: dict):
|
async def fire_event(event: str, payload: dict):
|
||||||
"""Dispatch an event to outbound webhooks and matching automations."""
|
"""Dispatch an event to outbound webhooks and matching automations."""
|
||||||
# v7.3.0: page.updated → in-app notification to followers (throttled).
|
# v7.3.0: page.updated → in-app notification to followers (throttled).
|
||||||
|
|||||||
+518
-84
@@ -2,7 +2,7 @@
|
|||||||
"openapi": "3.1.0",
|
"openapi": "3.1.0",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "FlowDeck",
|
"title": "FlowDeck",
|
||||||
"version": "7.8.0"
|
"version": "7.12.0"
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
"/auth/register": {
|
"/auth/register": {
|
||||||
@@ -10765,47 +10765,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/library/local-workspace-children/{item_id}": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"library"
|
|
||||||
],
|
|
||||||
"summary": "Library Local Workspace Children",
|
|
||||||
"description": "Return children of a local workspace item for tree expansion.",
|
|
||||||
"operationId": "library_local_workspace_children_api_library_local_workspace_children__item_id__get",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "item_id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "integer",
|
|
||||||
"title": "Item Id"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Successful Response",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation Error",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/HTTPValidationError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/library/children/{page_id}": {
|
"/api/library/children/{page_id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -10899,48 +10858,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/library/local-workspace": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"library"
|
|
||||||
],
|
|
||||||
"summary": "Library Local Workspace",
|
|
||||||
"description": "Return local workspace items (files/folders) formatted for Library display.",
|
|
||||||
"operationId": "library_local_workspace_api_library_local_workspace_get",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "workspace_id",
|
|
||||||
"in": "query",
|
|
||||||
"required": false,
|
|
||||||
"schema": {
|
|
||||||
"type": "integer",
|
|
||||||
"default": 0,
|
|
||||||
"title": "Workspace Id"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Successful Response",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation Error",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/HTTPValidationError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/library/workspace": {
|
"/api/library/workspace": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -16725,6 +16642,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -16974,6 +16902,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -17123,6 +17062,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -17233,6 +17183,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -17397,6 +17358,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -17462,6 +17434,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -17617,6 +17600,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -17727,6 +17721,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -17837,6 +17842,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -18003,6 +18019,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -18178,6 +18205,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -18288,6 +18326,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -18454,6 +18503,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -18564,6 +18624,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -18674,6 +18745,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -18849,6 +18931,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -18905,6 +18998,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -19015,6 +19119,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -19062,6 +19177,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -19109,6 +19235,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -19219,6 +19356,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -19275,6 +19423,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -19385,6 +19544,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -19495,6 +19665,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -19551,6 +19732,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -19715,6 +19907,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -19771,6 +19974,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -19881,6 +20095,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -20125,6 +20350,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -20217,6 +20453,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -20365,6 +20612,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -20421,6 +20679,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -20531,6 +20800,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -20753,6 +21033,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -20809,6 +21100,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -20919,6 +21221,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -21085,6 +21398,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -21195,6 +21519,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -21251,6 +21586,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -21361,6 +21707,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -21592,6 +21949,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -21648,6 +22016,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -21861,6 +22240,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -22367,6 +22757,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -22618,6 +23019,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -22674,6 +23086,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
@@ -22961,6 +23384,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"default": {},
|
||||||
|
"title": "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
"""A32 — smoke tests : un par route des 4 routers à 0 test.
|
||||||
|
|
||||||
|
`webhooks.py` (3), `notes.py` (2), `sidebar_config.py` (2), `github_routes.py` (2).
|
||||||
|
Aucun accès réseau réel : le secret webhook est piloté par monkeypatch et
|
||||||
|
`gitea.list_webhooks` est stubbé.
|
||||||
|
"""
|
||||||
|
from app.config import settings
|
||||||
|
|
||||||
|
OWNER, REPO = "smoke-a32", "repo-x"
|
||||||
|
|
||||||
|
|
||||||
|
# ── webhooks.py (0/3) ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_webhook_receive_ok_without_secret(client, monkeypatch):
|
||||||
|
monkeypatch.setattr(settings, "gitea_webhook_secret", "")
|
||||||
|
r = client.post("/api/webhook", json={"zen": "smoke"})
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json() == {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhook_receive_rejects_bad_signature(client, monkeypatch):
|
||||||
|
monkeypatch.setattr(settings, "gitea_webhook_secret", "s3cret")
|
||||||
|
r = client.post("/api/webhook", json={}) # pas d'en-tête X-Gitea-Signature
|
||||||
|
assert r.status_code == 401
|
||||||
|
assert r.json()["detail"] == "Invalid signature"
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhook_register_fails_fast_without_secret(client, monkeypatch):
|
||||||
|
# secret vide → 400 AVANT tout appel réseau (gitea.list_webhooks)
|
||||||
|
monkeypatch.setattr(settings, "gitea_webhook_secret", "")
|
||||||
|
r = client.post(f"/api/webhook/register/{OWNER}/{REPO}")
|
||||||
|
assert r.status_code == 400
|
||||||
|
assert "not configured" in r.json()["detail"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhook_status_stubbed(client, monkeypatch):
|
||||||
|
from app.services import gitea_client
|
||||||
|
|
||||||
|
async def _no_hooks(*_a, **_k):
|
||||||
|
return []
|
||||||
|
|
||||||
|
monkeypatch.setattr(gitea_client.gitea, "list_webhooks", _no_hooks)
|
||||||
|
r = client.get(f"/api/webhook/status/{OWNER}/{REPO}")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json() == {"registered": False}
|
||||||
|
|
||||||
|
|
||||||
|
# ── notes.py (0/2) ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_notes_get_renders_html(client):
|
||||||
|
r = client.get(f"/notes/{OWNER}/{REPO}")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert "text/html" in r.headers["content-type"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_notes_save_roundtrip(client):
|
||||||
|
payload = "note de smoke A32"
|
||||||
|
html_payload = "<b>&co</b>"
|
||||||
|
r = client.post(
|
||||||
|
f"/notes/{OWNER}/{REPO}", data={"content": f"{payload} {html_payload}"}
|
||||||
|
)
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert payload in r.text # contenu rendu
|
||||||
|
assert "<b>&co</b>" in r.text # échappé (A10 — pas d'HTML cru)
|
||||||
|
again = client.get(f"/notes/{OWNER}/{REPO}")
|
||||||
|
assert payload in again.text # persistée en base (upsert)
|
||||||
|
|
||||||
|
|
||||||
|
# ── sidebar_config.py (0/2) ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_sidebar_config_get(client):
|
||||||
|
r = client.get("/api/sidebar/config")
|
||||||
|
assert r.status_code == 200
|
||||||
|
cfg = r.json()["config"]
|
||||||
|
assert isinstance(cfg, dict) and "meetings" in cfg
|
||||||
|
|
||||||
|
|
||||||
|
def test_sidebar_config_put_persists_and_validates(client):
|
||||||
|
r = client.put(
|
||||||
|
"/api/sidebar/config",
|
||||||
|
json={"config": {"meetings": {"visible": False, "order": 2, "show_count": 5}}},
|
||||||
|
)
|
||||||
|
assert r.status_code == 200
|
||||||
|
back = client.get("/api/sidebar/config").json()["config"]
|
||||||
|
assert back["meetings"]["visible"] is False # relu depuis users.sidebar_config
|
||||||
|
|
||||||
|
bad = client.put("/api/sidebar/config", json={})
|
||||||
|
assert bad.status_code == 400 # config dict requis
|
||||||
|
|
||||||
|
# remise en état pour les autres tests
|
||||||
|
restore = client.put(
|
||||||
|
"/api/sidebar/config",
|
||||||
|
json={"config": {"meetings": {"visible": True, "order": 2, "show_count": 5}}},
|
||||||
|
)
|
||||||
|
assert restore.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
# ── github_routes.py (0/2) ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_github_status_unlinked(client):
|
||||||
|
r = client.get("/api/github/status")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json() == {"linked": False}
|
||||||
|
|
||||||
|
|
||||||
|
def test_github_disconnect_ok(client):
|
||||||
|
r = client.delete("/api/github/disconnect")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json() == {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
# ── library.py (1/10 → 10/10) ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_page(title: str, **cols) -> int:
|
||||||
|
"""Insert minimale (title/content/format + colonnes surnuméraires), id renvoyé."""
|
||||||
|
from app.db import get_conn
|
||||||
|
|
||||||
|
columns = ["workspace", "title", "content", "content_format", *cols]
|
||||||
|
placeholders = ", ".join("?" for _ in columns)
|
||||||
|
values = ["", title, "", "markdown", *cols.values()]
|
||||||
|
with get_conn() as conn:
|
||||||
|
cur = conn.execute(
|
||||||
|
f"INSERT INTO pages ({', '.join(columns)}) VALUES ({placeholders})", values
|
||||||
|
)
|
||||||
|
pid = cur.lastrowid
|
||||||
|
conn.commit()
|
||||||
|
return pid
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_all_lists_return_items(client):
|
||||||
|
"""Les 5 listes de la bibliothèque répondent {"items": [...]}."""
|
||||||
|
for path in ("/recents", "/favorites", "/published", "/private", "/workspace"):
|
||||||
|
r = client.get(f"/api/library{path}")
|
||||||
|
assert r.status_code == 200, (path, r.status_code)
|
||||||
|
assert isinstance(r.json()["items"], list), path
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_private_lists_seeded_page(client):
|
||||||
|
from app.db import get_conn
|
||||||
|
|
||||||
|
pid = _seed_page("A32 private page", parent_section="Private")
|
||||||
|
try:
|
||||||
|
items = client.get("/api/library/private").json()["items"]
|
||||||
|
assert "A32 private page" in [i["title"] for i in items]
|
||||||
|
finally:
|
||||||
|
with get_conn() as conn:
|
||||||
|
conn.execute("DELETE FROM pages WHERE id=?", (pid,))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_children_lists_child(client):
|
||||||
|
from app.db import get_conn
|
||||||
|
|
||||||
|
parent = _seed_page("A32 parent")
|
||||||
|
child = _seed_page("A32 child", parent_id=parent)
|
||||||
|
try:
|
||||||
|
items = client.get(f"/api/library/children/{parent}").json()["items"]
|
||||||
|
assert [i["title"] for i in items] == ["A32 child"]
|
||||||
|
finally:
|
||||||
|
with get_conn() as conn:
|
||||||
|
conn.execute("DELETE FROM pages WHERE id IN (?, ?)", (parent, child))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_local_workspace_routes_removed(client):
|
||||||
|
"""A32 → A30 : ces 2 routes lisaient `local_workspace_items`, une table
|
||||||
|
inexistante nulle part dans le codebase (500 systématique côté children,
|
||||||
|
500 d?s qu'un workspace existe côté local-workspace) et AUCUNE référence
|
||||||
|
front → supprimées avec `_format_size` devenu mort."""
|
||||||
|
assert client.get("/api/library/local-workspace-children/1").status_code == 404
|
||||||
|
assert client.get("/api/library/local-workspace").status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_repository_empty_and_keyed(client):
|
||||||
|
empty = client.get("/api/library/repository")
|
||||||
|
assert empty.status_code == 200 and empty.json() == {"items": []}
|
||||||
|
keyed = client.get("/api/library/repository?gitea_owner=acme&gitea_repo=flowdeck")
|
||||||
|
assert keyed.status_code == 200
|
||||||
|
assert isinstance(keyed.json()["items"], list)
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_lists_still_available_after_removal(client):
|
||||||
|
"""Les listes restantes (celles qu'utilise le front) répondent toujours."""
|
||||||
|
r = client.get("/api/library/recents")
|
||||||
|
assert r.status_code == 200 and isinstance(r.json()["items"], list)
|
||||||
Reference in New Issue
Block a user