feat(db): provision Postgres and add score schema migration

Wire in the txt_game_scores database provision and create the
players/games tables via a provision-scoped migration so Child 2
can implement the server-side score persistence.

- module.yml: requires postgres database (txt_game_scores) with
  env_map for host/port/user/password/name; empty env keys added
  so env-sync populates them after provisioning
- migrations/package.json: ESM, pg + typescript devdeps
- migrations/provision/postgres/000-init-scores.ts: creates
  players and games tables with idempotent IF NOT EXISTS guards
  and two supporting indexes (scoreboard sort, history lookup)

Server code, Dockerfile, and docker-compose.yml are unchanged —
server integration is deferred to the follow-on child task.

Task: 3dab55e3-9792-47a0-a4a6-ba7757517314
This commit is contained in:
2026-07-09 18:51:18 +02:00
parent 0139527147
commit cbec696c59
3 changed files with 80 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
{
"private": true,
"type": "module",
"dependencies": {
"pg": "*"
},
"devDependencies": {
"@types/node": "*",
"@types/pg": "*",
"typescript": "*"
}
}
@@ -0,0 +1,52 @@
import pg from "pg";
const client = new pg.Client({
host: process.env.PROVISION_HOST,
port: parseInt(process.env.PROVISION_PORT ?? "5432"),
user: process.env.PROVISION_USER,
password: process.env.PROVISION_PASSWORD,
database: process.env.PROVISION_DATABASE,
});
await client.connect();
try {
await client.query(`
CREATE TABLE IF NOT EXISTS players (
id TEXT PRIMARY KEY,
games_played INT NOT NULL DEFAULT 0,
games_won INT NOT NULL DEFAULT 0,
games_lost INT NOT NULL DEFAULT 0,
best_attempts INT,
current_streak INT NOT NULL DEFAULT 0,
best_streak INT NOT NULL DEFAULT 0,
last_played_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS games (
id BIGSERIAL PRIMARY KEY,
player_id TEXT NOT NULL REFERENCES players(id) ON DELETE CASCADE,
target INT NOT NULL,
attempts INT NOT NULL,
won BOOLEAN NOT NULL,
finished_at TIMESTAMPTZ NOT NULL DEFAULT now()
)
`);
await client.query(`
CREATE INDEX IF NOT EXISTS idx_players_scoreboard
ON players (best_attempts NULLS LAST, last_played_at)
`);
await client.query(`
CREATE INDEX IF NOT EXISTS idx_games_player_history
ON games (player_id, finished_at DESC)
`);
console.log("[txt-game migration-000] score schema created");
} finally {
await client.end();
}