feat: Feature 1.2 (NBA stats FastAPI service) + Feature 1.4 (database schema)

Feature 1.2: Python FastAPI microservice wrapping nba_api
- GET /stats/season-avg, /stats/last-n, /stats/splits, /players/search
- Redis caching (24hr/1hr/6hr/7day), 0.6s rate limiting, PRA derived stat
- 27 Python tests passing

Feature 1.4: Complete Supabase database schema
- 6 tables: users, picks, scan_sessions, bets, outcomes, performance
- RLS enabled on all tables with auth.uid() policies
- 3 triggers: auto-create user, updated_at, scan count reset
- 37 schema validation tests passing
- Migration SQL ready, pending manual apply (WSL2 DNS blocker)

Total: 92 tests (65 Node.js + 27 Python), all passing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-03-21 10:58:58 -04:00
parent 00409fd6cd
commit 3da1b4242c
27 changed files with 2360 additions and 16 deletions
+136
View File
@@ -0,0 +1,136 @@
require('dotenv').config();
const fs = require('fs');
const postgres = require('postgres');
const DB_PASSWORD = process.env.SUPABASE_DB_PASSWORD;
const PROJECT_REF = process.env.SUPABASE_URL.match(/https:\/\/(.+?)\.supabase/)[1];
// Use session mode (port 5432) for DDL statements
const sql = postgres({
host: `aws-0-us-east-1.pooler.supabase.com`,
port: 5432,
database: 'postgres',
username: `postgres.${PROJECT_REF}`,
password: DB_PASSWORD,
ssl: { rejectUnauthorized: false },
connection: { application_name: 'betonblk-migration' },
prepare: false,
});
async function applyMigration() {
// Test connection first
try {
const result = await sql`SELECT current_database(), current_user`;
console.log('Connected:', result[0]);
} catch (err) {
console.error('Connection failed:', err.message);
// Try transaction mode as fallback
console.log('Trying transaction mode on port 6543...');
const sql2 = postgres({
host: `aws-0-us-east-1.pooler.supabase.com`,
port: 6543,
database: 'postgres',
username: `postgres.${PROJECT_REF}`,
password: DB_PASSWORD,
ssl: { rejectUnauthorized: false },
prepare: false,
});
try {
const result2 = await sql2`SELECT current_database(), current_user`;
console.log('Connected via transaction mode:', result2[0]);
await runMigration(sql2);
await sql2.end();
return;
} catch (err2) {
console.error('Transaction mode also failed:', err2.message);
// Try direct connection
console.log('Trying direct connection...');
const sql3 = postgres({
host: `db.${PROJECT_REF}.supabase.co`,
port: 5432,
database: 'postgres',
username: 'postgres',
password: DB_PASSWORD,
ssl: { rejectUnauthorized: false },
prepare: false,
});
try {
const result3 = await sql3`SELECT current_database(), current_user`;
console.log('Connected directly:', result3[0]);
await runMigration(sql3);
await sql3.end();
return;
} catch (err3) {
console.error('Direct connection failed:', err3.message);
process.exit(1);
}
}
}
await runMigration(sql);
await sql.end();
}
async function runMigration(db) {
const migration = fs.readFileSync('supabase/migrations/001_initial_schema.sql', 'utf8');
const statements = splitStatements(migration);
console.log(`Applying ${statements.length} statements...`);
let ok = 0;
let errors = 0;
for (let i = 0; i < statements.length; i++) {
const stmt = statements[i].trim();
if (!stmt || stmt.startsWith('--')) continue;
try {
await db.unsafe(stmt);
ok++;
console.log(`[${i + 1}/${statements.length}] OK`);
} catch (err) {
errors++;
console.error(`[${i + 1}/${statements.length}] ERROR: ${err.message}`);
console.error(`Statement: ${stmt.substring(0, 120)}...`);
}
}
console.log(`Migration complete. ${ok} succeeded, ${errors} failed.`);
}
function splitStatements(sqlText) {
const results = [];
let current = '';
let inDollarQuote = false;
const lines = sqlText.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('--')) {
current += line + '\n';
continue;
}
const dollarCount = (line.match(/\$\$/g) || []).length;
if (dollarCount % 2 !== 0) {
inDollarQuote = !inDollarQuote;
}
current += line + '\n';
if (!inDollarQuote && trimmed.endsWith(';')) {
results.push(current);
current = '';
}
}
if (current.trim()) {
results.push(current);
}
return results;
}
applyMigration().catch(console.error);
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
# Start both BetonBLK services
# Usage: ./scripts/start.sh
set -e
echo "[BetonBLK] Starting services..."
# Start Python NBA stats service (port 8000)
echo "[BetonBLK] Starting NBA stats service on port 8000..."
cd nba-service
source venv/bin/activate
uvicorn app.main:app --host 0.0.0.0 --port 8000 &
NBA_PID=$!
cd ..
# Start Node.js API server (port 3000)
echo "[BetonBLK] Starting Node API server on port 3000..."
node src/server.js &
NODE_PID=$!
echo "[BetonBLK] Services running:"
echo " Node API: http://localhost:3000 (PID: $NODE_PID)"
echo " NBA Stats: http://localhost:8000 (PID: $NBA_PID)"
echo ""
echo "Press Ctrl+C to stop all services."
trap "kill $NBA_PID $NODE_PID 2>/dev/null; echo '[BetonBLK] Services stopped.'" EXIT
wait
+54
View File
@@ -0,0 +1,54 @@
require('dotenv').config();
const { createClient } = require('@supabase/supabase-js');
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_KEY
);
const EXPECTED_TABLES = ['users', 'picks', 'scan_sessions', 'bets', 'outcomes', 'performance'];
async function verifySchema() {
console.log('[BetonBLK] Verifying database schema...\n');
let allPassed = true;
// Check each table exists by querying it
for (const table of EXPECTED_TABLES) {
const { data, error } = await supabase.from(table).select('*').limit(0);
if (error) {
console.error(` FAIL: ${table}${error.message}`);
allPassed = false;
} else {
console.log(` OK: ${table}`);
}
}
console.log('');
// Test RLS: service role should be able to query all tables
console.log('Testing service role access...');
for (const table of EXPECTED_TABLES) {
const { error } = await supabase.from(table).select('*').limit(1);
if (error) {
console.error(` FAIL: service role cannot access ${table}${error.message}`);
allPassed = false;
} else {
console.log(` OK: service role can access ${table}`);
}
}
console.log('');
if (allPassed) {
console.log('ALL CHECKS PASSED. Schema is correctly applied.');
} else {
console.error('SOME CHECKS FAILED. Review errors above.');
process.exit(1);
}
}
verifySchema().catch((err) => {
console.error('Verification failed:', err.message);
process.exit(1);
});