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:
@@ -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);
|
||||
Executable
+30
@@ -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
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user