diff --git a/frontend/build-cf-pages.sh b/frontend/build-cf-pages.sh index 582a4d8..b20c938 100755 --- a/frontend/build-cf-pages.sh +++ b/frontend/build-cf-pages.sh @@ -3,7 +3,9 @@ if [ "$CF_PAGES_BRANCH" = "main" ]; then export VITE_API_URL="https://tabletennis.chamika.workers.dev/api" else - export VITE_API_URL="https://${CF_PAGES_BRANCH//\//-}-tabletennis.chamika.workers.dev/api" + BRANCH_NAME="${CF_PAGES_BRANCH//\//-}" + BRANCH_NAME="${BRANCH_NAME//_/-}" + export VITE_API_URL="https://${BRANCH_NAME}-tabletennis.chamika.workers.dev/api" fi echo "VITE_API_URL=$VITE_API_URL" diff --git a/worker/migrations/0001_remove_is_past.sql b/worker/migrations/0001_remove_is_past.sql new file mode 100644 index 0000000..f642a40 --- /dev/null +++ b/worker/migrations/0001_remove_is_past.sql @@ -0,0 +1,6 @@ +-- Migration: Remove is_past column from fixtures table +-- The is_past field will be computed dynamically in the API layer +-- based on comparing match_date with the current date + +-- Drop the is_past column from fixtures table +ALTER TABLE fixtures DROP COLUMN is_past; diff --git a/worker/migrations/README.md b/worker/migrations/README.md new file mode 100644 index 0000000..752c577 --- /dev/null +++ b/worker/migrations/README.md @@ -0,0 +1,72 @@ +# Database Migration: Remove is_past Column + +This migration removes the `is_past` column from the fixtures table. The field will be computed dynamically in the API layer based on comparing `match_date` with the current date. + +## Migration File + +Location: `worker/migrations/0001_remove_is_past.sql` + +## Applying the Migration + +### Development Environment + +```bash +cd worker +wrangler d1 migrations apply tabletennis-availability --local +``` + +### Staging Environment + +```bash +cd worker +wrangler d1 migrations apply tabletennis-availability-staging --env staging +``` + +### Production Environment + +```bash +cd worker +wrangler d1 migrations apply tabletennis-availability-prod --env production +``` + +## Verification + +After applying the migration, verify the schema: + +### Development +```bash +wrangler d1 execute tabletennis-availability --local --command="PRAGMA table_info(fixtures);" +``` + +### Staging +```bash +wrangler d1 execute tabletennis-availability-staging --env staging --command="PRAGMA table_info(fixtures);" +``` + +### Production +```bash +wrangler d1 execute tabletennis-availability-prod --env production --command="PRAGMA table_info(fixtures);" +``` + +The `is_past` column should no longer appear in the output. + +## Rollback + +If you need to rollback this migration, you can recreate the column with: + +```sql +ALTER TABLE fixtures ADD COLUMN is_past INTEGER DEFAULT 0; +``` + +However, note that the column values won't be automatically populated. You would need to run an UPDATE statement to set the values based on match_date. + +## Testing + +Run the test suite to ensure everything works correctly: + +```bash +cd worker +npm test +``` + +All tests should pass with the new implementation computing `is_past` dynamically in the API layer. diff --git a/worker/schema.sql b/worker/schema.sql index be22eaf..1d74b22 100644 --- a/worker/schema.sql +++ b/worker/schema.sql @@ -19,7 +19,6 @@ CREATE TABLE IF NOT EXISTS fixtures ( home_team TEXT NOT NULL, away_team TEXT NOT NULL, venue TEXT, - is_past INTEGER DEFAULT 0, created_at INTEGER NOT NULL, FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE ); diff --git a/worker/seed.sql b/worker/seed.sql index 0d192e1..8321062 100644 --- a/worker/seed.sql +++ b/worker/seed.sql @@ -28,12 +28,13 @@ INSERT INTO players (id, team_id, name, created_at) VALUES ('player-6', '00000000-0000-0000-0000-000000000000', 'Frank Foster', strftime('%s', 'now')); -- Insert fixtures (3 future, 2 past) -INSERT INTO fixtures (id, team_id, match_date, day_time, home_team, away_team, venue, is_past, created_at) VALUES - ('fixture-future-1', '00000000-0000-0000-0000-000000000000', date('now', '+7 days'), '19:30', 'Test Team E2E', 'Future Team A', 'Home Venue', 0, strftime('%s', 'now')), - ('fixture-future-2', '00000000-0000-0000-0000-000000000000', date('now', '+14 days'), '20:00', 'Future Team B', 'Test Team E2E', 'Away Venue', 0, strftime('%s', 'now')), - ('fixture-future-3', '00000000-0000-0000-0000-000000000000', date('now', '+21 days'), '19:45', 'Test Team E2E', 'Future Team C', 'Home Venue', 0, strftime('%s', 'now')), - ('fixture-past-1', '00000000-0000-0000-0000-000000000000', date('now', '-7 days'), '19:30', 'Past Team A', 'Test Team E2E', 'Away Venue', 1, strftime('%s', 'now')), - ('fixture-past-2', '00000000-0000-0000-0000-000000000000', date('now', '-14 days'), '20:00', 'Test Team E2E', 'Past Team B', 'Home Venue', 1, strftime('%s', 'now')); +-- Note: is_past is computed dynamically by the API based on match_date +INSERT INTO fixtures (id, team_id, match_date, day_time, home_team, away_team, venue, created_at) VALUES + ('fixture-future-1', '00000000-0000-0000-0000-000000000000', date('now', '+7 days'), '19:30', 'Test Team E2E', 'Future Team A', 'Home Venue', strftime('%s', 'now')), + ('fixture-future-2', '00000000-0000-0000-0000-000000000000', date('now', '+14 days'), '20:00', 'Future Team B', 'Test Team E2E', 'Away Venue', strftime('%s', 'now')), + ('fixture-future-3', '00000000-0000-0000-0000-000000000000', date('now', '+21 days'), '19:45', 'Test Team E2E', 'Future Team C', 'Home Venue', strftime('%s', 'now')), + ('fixture-past-1', '00000000-0000-0000-0000-000000000000', date('now', '-7 days'), '19:30', 'Past Team A', 'Test Team E2E', 'Away Venue', strftime('%s', 'now')), + ('fixture-past-2', '00000000-0000-0000-0000-000000000000', date('now', '-14 days'), '20:00', 'Test Team E2E', 'Past Team B', 'Home Venue', strftime('%s', 'now')); -- Initialize availability for all fixtures (all players available) INSERT INTO availability (id, fixture_id, player_id, is_available, updated_at) VALUES diff --git a/worker/src/database.integration.test.ts b/worker/src/database.integration.test.ts index 3bfb091..7c00995 100644 --- a/worker/src/database.integration.test.ts +++ b/worker/src/database.integration.test.ts @@ -99,28 +99,24 @@ describe('DatabaseService Integration Tests', () => { expect(fixture.venue).toBe('Test Venue'); }); - it('should mark past fixtures correctly', async () => { - const pastFixture = await db.createFixture( - 'team-123', - '2020-01-01', - 'Jan 1 Wed 19:00', - 'Home Team', - 'Away Team' - ); - - expect(pastFixture.is_past).toBe(1); - }); - - it('should mark future fixtures correctly', async () => { - const futureFixture = await db.createFixture( + it('should create a fixture with all required fields', async () => { + const fixture = await db.createFixture( 'team-123', '2030-12-31', 'Dec 31 Wed 19:00', 'Home Team', - 'Away Team' + 'Away Team', + 'Test Venue' ); - expect(futureFixture.is_past).toBe(0); + expect(fixture.id).toBeDefined(); + expect(fixture.team_id).toBe('team-123'); + expect(fixture.match_date).toBe('2030-12-31'); + expect(fixture.day_time).toBe('Dec 31 Wed 19:00'); + expect(fixture.home_team).toBe('Home Team'); + expect(fixture.away_team).toBe('Away Team'); + expect(fixture.venue).toBe('Test Venue'); + expect(fixture.created_at).toBeDefined(); }); it('should get fixtures for a team', async () => { @@ -133,7 +129,6 @@ describe('DatabaseService Integration Tests', () => { home_team: 'Home Team 1', away_team: 'Away Team 1', venue: null, - is_past: 0, created_at: Date.now() }, { @@ -144,7 +139,6 @@ describe('DatabaseService Integration Tests', () => { home_team: 'Home Team 2', away_team: 'Away Team 2', venue: 'Test Venue', - is_past: 0, created_at: Date.now() } ]; @@ -370,7 +364,7 @@ describe('DatabaseService Integration Tests', () => { ); expect(fixture.id).toBeDefined(); - expect(fixture.is_past).toBe(0); + expect(fixture.match_date).toBe('2026-01-15'); // Create availability await db.createAvailability(fixture.id, player1.id, true); @@ -396,7 +390,6 @@ describe('DatabaseService Integration Tests', () => { home_team: 'Home United', away_team: 'Away City', venue: 'Test Venue', - is_past: 0, created_at: Date.now() }; @@ -433,12 +426,11 @@ describe('DatabaseService Integration Tests', () => { mockD1.prepare = () => ({ bind: (...params: any[]) => { // Verify update was called with correct parameters - if (params.length === 4) { + if (params.length === 3) { updateCalled = true; expect(params[0]).toBe('2026-04-20'); // match_date expect(params[1]).toBe('Apr 20 19:00'); // day_time - expect(params[2]).toBe(0); // is_past (future date) - expect(params[3]).toBe(fixtureId); + expect(params[2]).toBe(fixtureId); } return { run: async () => ({ success: true }) @@ -451,26 +443,6 @@ describe('DatabaseService Integration Tests', () => { expect(updateCalled).toBe(true); }); - it('should mark fixture as past when updating to past date', async () => { - const fixtureId = 'fixture-123'; - let isPastValue: number | null = null; - - mockD1.prepare = () => ({ - bind: (...params: any[]) => { - if (params.length === 4) { - isPastValue = params[2]; // is_past parameter - } - return { - run: async () => ({ success: true }) - }; - } - }); - - await db.updateFixtureDate(fixtureId, '2025-01-01', 'Jan 1 20:00'); - - expect(isPastValue).toBe(1); - }); - it('should clear availability for fixture', async () => { const fixtureId = 'fixture-123'; let deleteCalled = false; @@ -505,7 +477,6 @@ describe('DatabaseService Integration Tests', () => { home_team: 'Home Team', away_team: 'Away Team', venue: null, - is_past: 0, created_at: Date.now() }; diff --git a/worker/src/database.ts b/worker/src/database.ts index 4ceb32f..1e9379b 100644 --- a/worker/src/database.ts +++ b/worker/src/database.ts @@ -1,5 +1,5 @@ -import type { Env, Team, Fixture, Player, Availability, FinalSelection } from './types'; -import { generateUUID, now, isPastDate } from './utils'; +import type { Env, Team, FixtureRow, Player, Availability, FinalSelection } from './types'; +import { generateUUID, now } from './utils'; /** * Database service for D1 operations @@ -52,17 +52,16 @@ export class DatabaseService { homeTeam: string, awayTeam: string, venue?: string - ): Promise { + ): Promise { const id = generateUUID(); const timestamp = now(); - const isPast = isPastDate(matchDate) ? 1 : 0; await this.db .prepare(` - INSERT INTO fixtures (id, team_id, match_date, day_time, home_team, away_team, venue, is_past, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO fixtures (id, team_id, match_date, day_time, home_team, away_team, venue, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) `) - .bind(id, teamId, matchDate, dayTime, homeTeam, awayTeam, venue || null, isPast, timestamp) + .bind(id, teamId, matchDate, dayTime, homeTeam, awayTeam, venue || null, timestamp) .run(); return { @@ -73,44 +72,41 @@ export class DatabaseService { home_team: homeTeam, away_team: awayTeam, venue: venue || null, - is_past: isPast, created_at: timestamp }; } - async getFixtures(teamId: string): Promise { + async getFixtures(teamId: string): Promise { const result = await this.db .prepare('SELECT * FROM fixtures WHERE team_id = ? ORDER BY match_date ASC') .bind(teamId) - .all(); + .all(); return result.results || []; } - async getFixture(fixtureId: string): Promise { + async getFixture(fixtureId: string): Promise { const result = await this.db .prepare('SELECT * FROM fixtures WHERE id = ?') .bind(fixtureId) - .first(); + .first(); return result; } - async getFixtureByTeams(teamId: string, homeTeam: string, awayTeam: string): Promise { + async getFixtureByTeams(teamId: string, homeTeam: string, awayTeam: string): Promise { const result = await this.db .prepare('SELECT * FROM fixtures WHERE team_id = ? AND home_team = ? AND away_team = ?') .bind(teamId, homeTeam, awayTeam) - .first(); + .first(); return result; } async updateFixtureDate(fixtureId: string, matchDate: string, dayTime: string): Promise { - const isPast = isPastDate(matchDate) ? 1 : 0; - await this.db - .prepare('UPDATE fixtures SET match_date = ?, day_time = ?, is_past = ? WHERE id = ?') - .bind(matchDate, dayTime, isPast, fixtureId) + .prepare('UPDATE fixtures SET match_date = ?, day_time = ? WHERE id = ?') + .bind(matchDate, dayTime, fixtureId) .run(); } @@ -239,14 +235,13 @@ export class DatabaseService { dayTime: string, playerIds: string[] ): Promise { - const isPast = isPastDate(matchDate) ? 1 : 0; const timestamp = now(); // Build batch of statements const statements = [ // Update fixture date - this.db.prepare('UPDATE fixtures SET match_date = ?, day_time = ?, is_past = ? WHERE id = ?') - .bind(matchDate, dayTime, isPast, fixtureId), + this.db.prepare('UPDATE fixtures SET match_date = ?, day_time = ? WHERE id = ?') + .bind(matchDate, dayTime, fixtureId), // Clear availability this.db.prepare('DELETE FROM availability WHERE fixture_id = ?') .bind(fixtureId), @@ -276,18 +271,17 @@ export class DatabaseService { awayTeam: string, venue: string | undefined, playerIds: string[] - ): Promise { + ): Promise { const fixtureId = generateUUID(); const timestamp = now(); - const isPast = isPastDate(matchDate) ? 1 : 0; // Build batch of statements const statements = [ // Create fixture this.db.prepare(` - INSERT INTO fixtures (id, team_id, match_date, day_time, home_team, away_team, venue, is_past, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - `).bind(fixtureId, teamId, matchDate, dayTime, homeTeam, awayTeam, venue || null, isPast, timestamp), + INSERT INTO fixtures (id, team_id, match_date, day_time, home_team, away_team, venue, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `).bind(fixtureId, teamId, matchDate, dayTime, homeTeam, awayTeam, venue || null, timestamp), ]; // Add availability inserts for each player @@ -310,7 +304,6 @@ export class DatabaseService { home_team: homeTeam, away_team: awayTeam, venue: venue || null, - is_past: isPast, created_at: timestamp }; } diff --git a/worker/src/index.test.ts b/worker/src/index.test.ts index c63caac..e7b363d 100644 --- a/worker/src/index.test.ts +++ b/worker/src/index.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import app from './index'; import * as scraper from './scraper'; import { DatabaseService } from './database'; -import type { Team, Fixture, Player } from './types'; +import type { Team, Fixture, FixtureRow, Player } from './types'; // Mock the scraper module vi.mock('./scraper'); @@ -18,16 +18,17 @@ describe('POST /api/availability/:teamId/sync', () => { id: mockTeamId, name: 'Test Team', elttl_url: mockElttlUrl, - created_at: '2024-01-01T00:00:00.000Z' + created_at: 1704067200000, + updated_at: 1704067200000 }; const mockPlayers: Player[] = [ - { id: 'player-1', team_id: mockTeamId, name: 'Player A', created_at: '2024-01-01T00:00:00.000Z' }, - { id: 'player-2', team_id: mockTeamId, name: 'Player B', created_at: '2024-01-01T00:00:00.000Z' }, - { id: 'player-3', team_id: mockTeamId, name: 'Player C', created_at: '2024-01-01T00:00:00.000Z' } + { id: 'player-1', team_id: mockTeamId, name: 'Player A', created_at: 1704067200000 }, + { id: 'player-2', team_id: mockTeamId, name: 'Player B', created_at: 1704067200000 }, + { id: 'player-3', team_id: mockTeamId, name: 'Player C', created_at: 1704067200000 } ]; - const mockExistingFixtures: Fixture[] = [ + const mockExistingFixtures: FixtureRow[] = [ { id: 'fixture-1', team_id: mockTeamId, @@ -35,9 +36,8 @@ describe('POST /api/availability/:teamId/sync', () => { day_time: 'Jan 15 Wed 18:45', home_team: 'Test Team', away_team: 'Opposition A', - venue: undefined, - is_past: 0, - created_at: '2024-01-01T00:00:00.000Z' + venue: null, + created_at: 1704067200000 }, { id: 'fixture-2', @@ -46,9 +46,8 @@ describe('POST /api/availability/:teamId/sync', () => { day_time: 'Jan 22 Wed 18:45', home_team: 'Opposition B', away_team: 'Test Team', - venue: undefined, - is_past: 0, - created_at: '2024-01-01T00:00:00.000Z' + venue: null, + created_at: 1704067200000 } ]; @@ -342,7 +341,7 @@ describe('POST /api/availability/:teamId/sync', () => { .mockResolvedValueOnce(mockExistingFixtures[0]) .mockResolvedValueOnce(null); - const newFixture: Fixture = { + const newFixture: FixtureRow = { id: 'fixture-3', team_id: mockTeamId, match_date: '2026-01-29', @@ -350,8 +349,7 @@ describe('POST /api/availability/:teamId/sync', () => { home_team: 'Test Team', away_team: 'Opposition C', venue: 'VENUE1', - is_past: 0, - created_at: '2024-01-01T00:00:00.000Z' + created_at: 1704067200000 }; mockDbInstance.createFixture.mockResolvedValue(newFixture); diff --git a/worker/src/index.ts b/worker/src/index.ts index 82af993..75cd1a8 100644 --- a/worker/src/index.ts +++ b/worker/src/index.ts @@ -1,9 +1,9 @@ import { Hono } from 'hono'; import { cors } from 'hono/cors'; -import type { Env, ImportTeamRequest, ImportTeamResponse, SyncResponse } from './types'; +import type { Env, ImportTeamRequest, ImportTeamResponse, SyncResponse, Fixture } from './types'; import { DatabaseService } from './database'; import { scrapeELTTLTeam } from './scraper'; -import { isValidELTTLUrl, parseMatchDate } from './utils'; +import { isValidELTTLUrl, parseMatchDate, isPastDate } from './utils'; const app = new Hono<{ Bindings: Env }>(); @@ -259,13 +259,19 @@ app.get('/api/availability/:teamId', async (c) => { } // Get fixtures, players, availability, and final selections - const [fixtures, players, availability, finalSelections] = await Promise.all([ + const [fixtureRows, players, availability, finalSelections] = await Promise.all([ db.getFixtures(teamId), db.getPlayers(teamId), db.getAvailability(teamId), db.getFinalSelections(teamId) ]); + // Add computed is_past field to fixtures + const fixtures: Fixture[] = fixtureRows.map(f => ({ + ...f, + is_past: isPastDate(f.match_date) ? 1 : 0 + })); + // Transform availability into a map const availabilityMap: Record = {}; for (const avail of availability) { @@ -448,12 +454,18 @@ app.get('/api/availability/:teamId/summary', async (c) => { } // Get all data - const [fixtures, players, finalSelections] = await Promise.all([ + const [fixtureRows, players, finalSelections] = await Promise.all([ db.getFixtures(teamId), db.getPlayers(teamId), db.getFinalSelections(teamId) ]); + // Add computed is_past field to fixtures + const fixtures: Fixture[] = fixtureRows.map(f => ({ + ...f, + is_past: isPastDate(f.match_date) ? 1 : 0 + })); + // Calculate summary for each player const summary = players.map(player => { let gamesPlayed = 0; diff --git a/worker/src/types.ts b/worker/src/types.ts index 6d406a9..840f4a0 100644 --- a/worker/src/types.ts +++ b/worker/src/types.ts @@ -4,7 +4,7 @@ export interface Env { DB: D1Database; } -// Database models +// Database models (row types - what's stored in D1) export interface Team { id: string; name: string; @@ -13,7 +13,7 @@ export interface Team { updated_at: number; } -export interface Fixture { +export interface FixtureRow { id: string; team_id: string; match_date: string; @@ -21,7 +21,6 @@ export interface Fixture { home_team: string; away_team: string; venue: string | null; - is_past: number; // SQLite uses INTEGER for boolean (0 or 1) created_at: number; } @@ -47,6 +46,11 @@ export interface FinalSelection { selected_at: number; } +// API response types (what the API returns to clients) +export interface Fixture extends FixtureRow { + is_past: number; // Computed field: 0 or 1 +} + // API request/response types export interface ImportTeamRequest { elttlUrl: string;