93182fa251
API surface for the web replay viewer to come:
- `POST /api/replays` — auth required; persists the JSON body
verbatim, mints a server-side UUID, returns `{id}`. Three columns
(final_score, time_seconds, recorded_at) are projected out of the
payload at insert time so list endpoints don't have to scan blobs.
- `GET /api/replays/recent` — public; returns the N most-recent
replays across users (limit defaults to 20, capped at 50). Joins
the username so the feed reads as "AliceWon · 2:14 win".
- `GET /api/replays/:id` — public; returns the full replay JSON
the desktop client uploaded.
Migration `002_replays.sql` adds the `replays` table with indexes
on `received_at DESC` (recent feed) and `user_id` (per-user views).
Schema-version compatibility is the playback side's responsibility,
matching the desktop's existing `schema_version` gate — the server
just stores and serves whatever JSON came in.
`AppError::NotFound` added so `GET /api/replays/:id` can return a
proper 404 instead of an internal-server-error.
`.sqlx` cache regenerated for the new `query!` invocations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
69 lines
1.9 KiB
JSON
69 lines
1.9 KiB
JSON
{
|
|
"db_name": "SQLite",
|
|
"query": "SELECT\n r.id AS \"id!: String\",\n u.username AS \"username!: String\",\n r.seed AS \"seed!: i64\",\n r.draw_mode AS \"draw_mode!: String\",\n r.mode AS \"mode!: String\",\n r.time_seconds AS \"time_seconds!: i64\",\n r.final_score AS \"final_score!: i64\",\n r.recorded_at AS \"recorded_at!: String\",\n r.received_at AS \"received_at!: String\"\n FROM replays r\n JOIN users u ON u.id = r.user_id\n ORDER BY r.received_at DESC\n LIMIT ?",
|
|
"describe": {
|
|
"columns": [
|
|
{
|
|
"name": "id!: String",
|
|
"ordinal": 0,
|
|
"type_info": "Text"
|
|
},
|
|
{
|
|
"name": "username!: String",
|
|
"ordinal": 1,
|
|
"type_info": "Text"
|
|
},
|
|
{
|
|
"name": "seed!: i64",
|
|
"ordinal": 2,
|
|
"type_info": "Integer"
|
|
},
|
|
{
|
|
"name": "draw_mode!: String",
|
|
"ordinal": 3,
|
|
"type_info": "Text"
|
|
},
|
|
{
|
|
"name": "mode!: String",
|
|
"ordinal": 4,
|
|
"type_info": "Text"
|
|
},
|
|
{
|
|
"name": "time_seconds!: i64",
|
|
"ordinal": 5,
|
|
"type_info": "Integer"
|
|
},
|
|
{
|
|
"name": "final_score!: i64",
|
|
"ordinal": 6,
|
|
"type_info": "Integer"
|
|
},
|
|
{
|
|
"name": "recorded_at!: String",
|
|
"ordinal": 7,
|
|
"type_info": "Text"
|
|
},
|
|
{
|
|
"name": "received_at!: String",
|
|
"ordinal": 8,
|
|
"type_info": "Text"
|
|
}
|
|
],
|
|
"parameters": {
|
|
"Right": 1
|
|
},
|
|
"nullable": [
|
|
true,
|
|
false,
|
|
false,
|
|
false,
|
|
false,
|
|
false,
|
|
false,
|
|
false,
|
|
false
|
|
]
|
|
},
|
|
"hash": "3a9bd2e51b2389da5b7e85f26806fcffa896748e0b589d216cf60827fc3857a9"
|
|
}
|