perf(api): хелпер ответа из готовых JSON-строк и SQL-выражение строки
Добавлены list_json_response (обёртка BaseListResponse из готовых строк), ROW_COLUMNS, row_json_select и fetch_row_texts для тяжёлых GET (AURORA-1848). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0175759f95
commit
7094fe839f
14
api/src/api/v1/json_response.py
Normal file
14
api/src/api/v1/json_response.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
"""Ответ тяжёлых GET из готовых JSON-строк Postgres (AURORA-1848, design D3)."""
|
||||||
|
from fastapi import Response
|
||||||
|
|
||||||
|
|
||||||
|
def list_json_response(rows: list[str], headers: dict[str, str] | None = None) -> Response:
|
||||||
|
"""Обёртка как у BaseListResponse при успехе; rows — уже сериализованные строки ответа."""
|
||||||
|
body = b"".join((
|
||||||
|
b'{"success":true,"message":null,"result":[',
|
||||||
|
",".join(rows).encode(),
|
||||||
|
b'],"count":',
|
||||||
|
str(len(rows)).encode(),
|
||||||
|
b"}",
|
||||||
|
))
|
||||||
|
return Response(content=body, media_type="application/json", headers=headers)
|
||||||
17
api/src/repository/row_json.py
Normal file
17
api/src/repository/row_json.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
"""Строка ответа тяжёлого GET как текст JSON из Postgres (AURORA-1848, design D1)."""
|
||||||
|
from sqlalchemy import text
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
ROW_COLUMNS = "row_type, depth, sort_order, data"
|
||||||
|
|
||||||
|
|
||||||
|
def row_json_select(row_type: str = "row_type", depth: str = "depth") -> str:
|
||||||
|
"""json (не jsonb) сохраняет порядок ключей; ::text обходит кодек json.loads у asyncpg."""
|
||||||
|
return (
|
||||||
|
f"SELECT json_build_object('row_type', {row_type}, 'depth', {depth}, "
|
||||||
|
f"'data', data, 'sort_order', sort_order)::text"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_row_texts(db: AsyncSession, sql: str, params: dict) -> list[str]:
|
||||||
|
return list((await db.execute(text(sql), params)).scalars().all())
|
||||||
41
api/tests/unit/test_json_list_response.py
Normal file
41
api/tests/unit/test_json_list_response.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from src.api.v1.json_response import list_json_response
|
||||||
|
from src.repository.row_json import ROW_COLUMNS, row_json_select
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_json_response_empty():
|
||||||
|
r = list_json_response([])
|
||||||
|
assert json.loads(r.body) == {"success": True, "message": None, "result": [], "count": 0}
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_json_response_rows_order_count_escaping():
|
||||||
|
rows = [
|
||||||
|
'{"row_type":"META","depth":-1,"data":{"a":null},"sort_order":null}',
|
||||||
|
r'{"row_type": "INPUT", "depth": 3, "data": {"t": "Кавычка \" и \\ слэш\nперенос"}, "sort_order": 1}',
|
||||||
|
]
|
||||||
|
body = json.loads(list_json_response(rows).body)
|
||||||
|
assert list(body) == ["success", "message", "result", "count"]
|
||||||
|
assert body["count"] == 2
|
||||||
|
assert [row["row_type"] for row in body["result"]] == ["META", "INPUT"]
|
||||||
|
assert body["result"][0]["data"] == {"a": None}
|
||||||
|
assert body["result"][1]["data"]["t"] == 'Кавычка " и \\ слэш\nперенос'
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_json_response_media_type_and_headers():
|
||||||
|
r = list_json_response(["{}"], headers={"X-DB-Time-Ms": "1.50"})
|
||||||
|
assert r.media_type == "application/json"
|
||||||
|
assert r.headers["content-type"] == "application/json"
|
||||||
|
assert r.headers["x-db-time-ms"] == "1.50"
|
||||||
|
assert r.headers["content-length"] == str(len(r.body))
|
||||||
|
|
||||||
|
|
||||||
|
def test_row_json_select_key_order():
|
||||||
|
assert ROW_COLUMNS == "row_type, depth, sort_order, data"
|
||||||
|
assert row_json_select() == (
|
||||||
|
"SELECT json_build_object('row_type', row_type, 'depth', depth, "
|
||||||
|
"'data', data, 'sort_order', sort_order)::text"
|
||||||
|
)
|
||||||
|
assert row_json_select(row_type="'INPUT'::VARCHAR", depth="3").startswith(
|
||||||
|
"SELECT json_build_object('row_type', 'INPUT'::VARCHAR, 'depth', 3, "
|
||||||
|
)
|
||||||
Loading…
x
Reference in New Issue
Block a user