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