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-ba7757517314feat/postgres-provision
| @@ -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(); | |||||
| } | |||||
| @@ -11,7 +11,23 @@ docker: | |||||
| context: . | context: . | ||||
| dockerfile: Dockerfile | dockerfile: Dockerfile | ||||
| requires: | |||||
| - provider: postgres | |||||
| type: database | |||||
| name: txt_game_scores | |||||
| env_map: | |||||
| TXT_GAME_DB_HOST: host | |||||
| TXT_GAME_DB_PORT: port | |||||
| TXT_GAME_DB_USER: user | |||||
| TXT_GAME_DB_PASSWORD: password | |||||
| TXT_GAME_DB_NAME: database | |||||
| env: | env: | ||||
| DOMAIN: | DOMAIN: | ||||
| from: node.domain | from: node.domain | ||||
| default: localhost | default: localhost | ||||
| TXT_GAME_DB_HOST: | |||||
| TXT_GAME_DB_PORT: | |||||
| TXT_GAME_DB_USER: | |||||
| TXT_GAME_DB_PASSWORD: | |||||
| TXT_GAME_DB_NAME: | |||||