Files
tt-booking/scripts/setup-database.ts
T

663 lines
19 KiB
TypeScript

import Database from 'better-sqlite3';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import * as schema from '../lib/db/schema';
import { sql, eq } from 'drizzle-orm';
import { randomUUID } from 'crypto';
import bcrypt from 'bcryptjs';
const sqlite = new Database('./sqlite.db');
const db = drizzle(sqlite, { schema });
interface SetupOptions {
reset?: boolean;
seedData?: boolean;
verbose?: boolean;
}
async function setupDatabase(options: SetupOptions = {}) {
const { reset = false, seedData = true, verbose = false } = options;
try {
console.log('🚀 Starting database setup...\n');
if (reset) {
await resetTables(verbose);
}
await createTables(verbose);
await seedBasicData(verbose);
if (seedData) {
await seedSampleData(verbose);
}
console.log('✅ Database setup completed successfully!\n');
// Print summary
await printDatabaseSummary();
} catch (error) {
console.error('❌ Database setup failed:', error);
throw error;
} finally {
sqlite.close();
}
}
async function resetTables(verbose: boolean) {
if (verbose) console.log('🗑️ Resetting database tables...');
const tables = [
'activity_logs',
'metrics',
'bookings',
'announcements',
'time_slots',
'settings',
'courts',
'users',
'__drizzle_migrations',
'__old_push_courts',
'__old_push_users',
];
for (const table of tables) {
try {
await db.run(sql.raw(`DROP TABLE IF EXISTS ${table}`));
if (verbose) console.log(` ✓ Dropped table: ${table}`);
} catch (error) {
if (verbose) console.log(` - Table ${table} doesn't exist or error dropping`);
}
}
console.log('✅ Tables reset complete\n');
}
async function createTables(verbose: boolean) {
if (verbose) console.log('🏗️ Creating database tables...');
// Users table
await db.run(sql`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
surname TEXT NOT NULL,
password TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user' CHECK (role IN ('user', 'admin')),
theme_preference TEXT DEFAULT 'system' CHECK (theme_preference IN ('light', 'dark', 'system')),
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
// Courts table
await db.run(sql`
CREATE TABLE IF NOT EXISTS courts (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
is_active INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
// Settings table
await db.run(sql`
CREATE TABLE IF NOT EXISTS settings (
id TEXT PRIMARY KEY,
key TEXT NOT NULL UNIQUE,
value TEXT NOT NULL,
updated_at INTEGER NOT NULL
)
`);
// Time slots table
await db.run(sql`
CREATE TABLE IF NOT EXISTS time_slots (
id TEXT PRIMARY KEY,
day_of_week INTEGER NOT NULL,
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
is_active INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
// Bookings table
await db.run(sql`
CREATE TABLE IF NOT EXISTS bookings (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
court_id TEXT NOT NULL REFERENCES courts(id) ON DELETE CASCADE,
date TEXT NOT NULL,
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'cancelled')),
notes TEXT,
partner_name TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
// Announcements table
await db.run(sql`
CREATE TABLE IF NOT EXISTS announcements (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
is_active INTEGER NOT NULL DEFAULT 1,
priority TEXT NOT NULL DEFAULT 'medium' CHECK (priority IN ('low', 'medium', 'high')),
expires_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
// Activity logs table
await db.run(sql`
CREATE TABLE IF NOT EXISTS activity_logs (
id TEXT PRIMARY KEY,
user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
action TEXT NOT NULL,
entity_type TEXT NOT NULL,
entity_id TEXT,
details TEXT,
ip_address TEXT,
user_agent TEXT,
created_at INTEGER NOT NULL
)
`);
// Metrics table
await db.run(sql`
CREATE TABLE IF NOT EXISTS metrics (
id TEXT PRIMARY KEY,
metric_type TEXT NOT NULL,
period TEXT NOT NULL,
value INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
if (verbose) console.log(' ✓ All tables created successfully');
console.log('✅ Database schema ready\n');
}
async function seedBasicData(verbose: boolean) {
console.log('🌱 Seeding essential data...');
const now = Date.now();
// Check if users already exist
const existingUsers = await db.select().from(schema.users);
if (existingUsers.length === 0) {
// Create admin user
const adminPassword = await bcrypt.hash('admin123', 12);
const adminId = randomUUID();
await db.insert(schema.users).values({
id: adminId,
email: 'admin@tabletennis.com',
name: 'Admin',
surname: 'User',
password: adminPassword,
role: 'admin',
themePreference: 'system',
createdAt: new Date(now),
updatedAt: new Date(now),
});
// Create test user
const userPassword = await bcrypt.hash('user123', 12);
const userId = randomUUID();
await db.insert(schema.users).values({
id: userId,
email: 'user@tabletennis.com',
name: 'Test',
surname: 'User',
password: userPassword,
role: 'user',
themePreference: 'system',
createdAt: new Date(now),
updatedAt: new Date(now),
});
if (verbose) console.log(' ✓ Created admin and test users');
} else {
if (verbose) console.log(' - Users already exist, skipping user creation');
}
// Check if courts already exist
const existingCourts = await db.select().from(schema.courts);
if (existingCourts.length === 0) {
// Create courts
const courtIds = [randomUUID(), randomUUID(), randomUUID()];
await db.insert(schema.courts).values([
{
id: courtIds[0],
name: 'Court 1',
isActive: true,
createdAt: new Date(now),
updatedAt: new Date(now),
},
{
id: courtIds[1],
name: 'Court 2',
isActive: true,
createdAt: new Date(now),
updatedAt: new Date(now),
},
{
id: courtIds[2],
name: 'Court 3',
isActive: true,
createdAt: new Date(now),
updatedAt: new Date(now),
},
]);
if (verbose) console.log(' ✓ Created 3 courts');
} else {
if (verbose) console.log(' - Courts already exist, skipping court creation');
}
// Insert system settings
const defaultSettings = [
{ key: 'booking_window_days', value: '14' },
{ key: 'max_booking_duration_hours', value: '2' },
{ key: 'max_bookings_per_user_per_hour_per_day', value: '1' },
{ key: 'allow_booking_modifications', value: 'true' },
{ key: 'booking_modification_hours_before', value: '2' },
{ key: 'min_booking_duration_minutes', value: '60' },
{ key: 'booking_start_time', value: '08:00' },
{ key: 'booking_end_time', value: '22:00' },
{ key: 'allow_weekend_bookings', value: 'true' },
{ key: 'facility_name', value: 'Table Tennis Club' },
{ key: 'facility_email', value: 'info@tabletennis.com' },
{ key: 'facility_phone', value: '+353-1-234-5678' },
];
for (const setting of defaultSettings) {
const existingSetting = await db
.select()
.from(schema.settings)
.where(eq(schema.settings.key, setting.key))
.limit(1);
if (existingSetting.length === 0) {
await db.insert(schema.settings).values({
id: randomUUID(),
key: setting.key,
value: setting.value,
updatedAt: new Date(now),
});
if (verbose) console.log(` ✓ Setting: ${setting.key} = ${setting.value}`);
}
}
// Check if time slots already exist
const existingTimeSlots = await db.select().from(schema.timeSlots);
if (existingTimeSlots.length === 0) {
// Create time slots - Operating hours for each day
const timeSlotData = [
// Sunday: 12:00 - 17:00 (shorter hours)
{ dayOfWeek: 0, startTime: '12:00', endTime: '17:00' },
// Monday to Thursday: 18:00 - 23:00 (evening sessions)
{ dayOfWeek: 1, startTime: '18:00', endTime: '23:00' },
{ dayOfWeek: 2, startTime: '18:00', endTime: '23:00' },
{ dayOfWeek: 3, startTime: '18:00', endTime: '23:00' },
{ dayOfWeek: 4, startTime: '18:00', endTime: '23:00' },
// Friday: 17:00 - 22:00 (earlier end)
{ dayOfWeek: 5, startTime: '17:00', endTime: '22:00' },
// Saturday: 10:00 - 18:00 (full day weekend)
{ dayOfWeek: 6, startTime: '10:00', endTime: '18:00' },
];
for (const slot of timeSlotData) {
await db.insert(schema.timeSlots).values({
id: randomUUID(),
dayOfWeek: slot.dayOfWeek,
startTime: slot.startTime,
endTime: slot.endTime,
isActive: true,
createdAt: new Date(now),
updatedAt: new Date(now),
});
}
if (verbose) console.log(' ✓ Created time slots for all days');
} else {
if (verbose) console.log(' - Time slots already exist, skipping time slot creation');
}
// Check if announcements already exist
const existingAnnouncements = await db.select().from(schema.announcements);
if (existingAnnouncements.length === 0) {
// Create essential announcements
const essentialAnnouncements = [
{
id: randomUUID(),
title: 'Welcome to Table Tennis Booking System!',
content:
'Book your court times easily and manage your games efficiently. Check the time slots for availability and remember to arrive 5 minutes early.',
isActive: true,
priority: 'high' as const,
expiresAt: null,
createdAt: new Date(now),
updatedAt: new Date(now),
},
{
id: randomUUID(),
title: 'Booking Guidelines',
content:
'Maximum booking duration is 2 hours. Please cancel bookings you cannot attend to allow others to use the courts.',
isActive: true,
priority: 'medium' as const,
expiresAt: null,
createdAt: new Date(now),
updatedAt: new Date(now),
},
];
await db.insert(schema.announcements).values(essentialAnnouncements);
if (verbose) console.log(' ✓ Created essential announcements');
} else {
if (verbose) console.log(' - Announcements already exist, skipping announcement creation');
}
// Initialize monthly metrics if they don't exist
const currentMonth = new Date().toISOString().substring(0, 7);
const existingMetrics = await db
.select()
.from(schema.metrics)
.where(eq(schema.metrics.period, currentMonth))
.limit(1);
if (existingMetrics.length === 0) {
await db.insert(schema.metrics).values({
id: randomUUID(),
metricType: 'monthly_bookings',
period: currentMonth,
value: 0,
createdAt: new Date(now),
updatedAt: new Date(now),
});
if (verbose) console.log(' ✓ Initialized monthly metrics');
} else {
if (verbose) console.log(' - Monthly metrics already exist');
}
console.log('✅ Essential data seeded\n');
}
async function seedSampleData(verbose: boolean) {
console.log('🎭 Seeding sample data...');
const now = Date.now();
const users = await db.select().from(schema.users);
const courts = await db.select().from(schema.courts);
const adminUser = users.find((u) => u.role === 'admin');
const regularUser = users.find((u) => u.role === 'user');
if (!adminUser || !regularUser) {
console.log('⚠️ No users found for sample data');
return;
}
// Sample bookings for the next few days
const today = new Date();
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
const dayAfter = new Date(today.getTime() + 48 * 60 * 60 * 1000);
const sampleBookings = [
{
id: randomUUID(),
userId: regularUser.id,
courtId: courts[0].id,
date: today.toISOString().split('T')[0],
startTime: '19:00',
endTime: '20:00',
status: 'active' as const,
notes: 'Regular evening practice session',
partnerName: 'John Smith',
createdAt: new Date(now - 2 * 60 * 60 * 1000),
updatedAt: new Date(now - 2 * 60 * 60 * 1000),
},
{
id: randomUUID(),
userId: regularUser.id,
courtId: courts[1]?.id || courts[0].id,
date: tomorrow.toISOString().split('T')[0],
startTime: '20:00',
endTime: '21:00',
status: 'active' as const,
notes: 'Tournament preparation',
partnerName: null,
createdAt: new Date(now - 1 * 60 * 60 * 1000),
updatedAt: new Date(now - 1 * 60 * 60 * 1000),
},
{
id: randomUUID(),
userId: adminUser.id,
courtId: courts[2]?.id || courts[0].id,
date: dayAfter.toISOString().split('T')[0],
startTime: '18:00',
endTime: '20:00',
status: 'active' as const,
notes: 'Staff training session',
partnerName: 'Staff Team',
createdAt: new Date(now - 30 * 60 * 1000),
updatedAt: new Date(now - 30 * 60 * 1000),
},
];
await db.insert(schema.bookings).values(sampleBookings);
// Sample activity logs
const sampleLogs = [
{
id: randomUUID(),
userId: adminUser.id,
action: 'login',
entityType: 'user',
entityId: adminUser.id,
details: JSON.stringify({
email: adminUser.email,
role: adminUser.role,
loginMethod: 'password',
}),
ipAddress: '192.168.1.100',
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
createdAt: new Date(now - 3 * 60 * 60 * 1000),
},
{
id: randomUUID(),
userId: regularUser.id,
action: 'create_booking',
entityType: 'booking',
entityId: sampleBookings[0].id,
details: JSON.stringify({
courtId: courts[0].id,
courtName: courts[0].name,
date: today.toISOString().split('T')[0],
startTime: '19:00',
endTime: '20:00',
}),
ipAddress: '192.168.1.101',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15',
createdAt: new Date(now - 2 * 60 * 60 * 1000),
},
];
await db.insert(schema.activityLogs).values(sampleLogs);
// Additional announcements
const additionalAnnouncements = [
{
id: randomUUID(),
title: 'New Court Rules',
content:
'Please remember to clean up after your sessions and respect the time limits. Equipment should be returned to the storage area.',
isActive: true,
priority: 'medium' as const,
expiresAt: new Date(now + 7 * 24 * 60 * 60 * 1000), // 1 week from now
createdAt: new Date(now - 4 * 60 * 60 * 1000),
updatedAt: new Date(now - 4 * 60 * 60 * 1000),
},
{
id: randomUUID(),
title: 'Tournament Sign-ups Open!',
content:
'The annual table tennis tournament sign-ups are now open! Register by the end of this month. Prizes for winners in each category.',
isActive: true,
priority: 'high' as const,
expiresAt: new Date(now + 30 * 24 * 60 * 60 * 1000), // 30 days from now
createdAt: new Date(now - 24 * 60 * 60 * 1000),
updatedAt: new Date(now - 24 * 60 * 60 * 1000),
},
{
id: randomUUID(),
title: 'Equipment Maintenance',
content:
'New paddles and balls have been added to the equipment collection. Old equipment will be replaced gradually.',
isActive: true,
priority: 'low' as const,
expiresAt: new Date(now + 14 * 24 * 60 * 60 * 1000), // 2 weeks from now
createdAt: new Date(now - 6 * 60 * 60 * 1000),
updatedAt: new Date(now - 6 * 60 * 60 * 1000),
},
];
await db.insert(schema.announcements).values(additionalAnnouncements);
if (verbose) {
console.log(` ✓ Created ${sampleBookings.length} sample bookings`);
console.log(` ✓ Created ${sampleLogs.length} activity logs`);
console.log(` ✓ Created ${additionalAnnouncements.length} additional announcements`);
}
console.log('✅ Sample data seeded\n');
}
async function printDatabaseSummary() {
console.log('📊 Database Summary:');
console.log('═══════════════════════════════════════\n');
const users = await db.select().from(schema.users);
const courts = await db.select().from(schema.courts);
const bookings = await db.select().from(schema.bookings);
const announcements = await db.select().from(schema.announcements);
const timeSlots = await db.select().from(schema.timeSlots);
const settings = await db.select().from(schema.settings);
console.log(`👥 Users: ${users.length}`);
users.forEach((user) => {
console.log(`${user.name} ${user.surname} (${user.email}) - ${user.role}`);
});
console.log(`\n🏓 Courts: ${courts.length}`);
courts.forEach((court) => {
console.log(`${court.name} - ${court.isActive ? 'Active' : 'Inactive'}`);
});
console.log(`\n📅 Bookings: ${bookings.length}`);
if (bookings.length > 0) {
console.log(' Recent bookings:');
bookings.slice(0, 3).forEach((booking) => {
console.log(`${booking.date} ${booking.startTime}-${booking.endTime} (${booking.status})`);
});
}
console.log(`\n📢 Announcements: ${announcements.length}`);
const activeAnnouncements = announcements.filter((a) => a.isActive);
console.log(` • Active: ${activeAnnouncements.length}`);
console.log(`\n⏰ Time Slots: ${timeSlots.length}`);
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
timeSlots.forEach((slot) => {
console.log(`${dayNames[slot.dayOfWeek]}: ${slot.startTime}-${slot.endTime}`);
});
console.log(`\n⚙️ Settings: ${settings.length} configured`);
console.log('\n💡 Login Credentials:');
console.log(' Admin: admin@tabletennis.com / admin123');
console.log(' User: user@tabletennis.com / user123');
console.log('\n🚀 Ready to start! Run: npm run dev');
console.log('═══════════════════════════════════════\n');
}
// Parse command line arguments
function parseArgs() {
const args = process.argv.slice(2);
const options: SetupOptions = {};
if (args.includes('--reset') || args.includes('-r')) {
options.reset = true;
}
if (args.includes('--no-sample-data') || args.includes('--essential-only')) {
options.seedData = false;
}
if (args.includes('--verbose') || args.includes('-v')) {
options.verbose = true;
}
if (args.includes('--help') || args.includes('-h')) {
console.log(`
Table Tennis Booking System - Database Setup
Usage: tsx scripts/setup-database.ts [options]
Options:
--reset, -r Reset/drop all tables before setup
--no-sample-data Only seed essential data (no sample bookings/logs)
--essential-only Same as --no-sample-data
--verbose, -v Show detailed output
--help, -h Show this help message
Examples:
tsx scripts/setup-database.ts # Full setup with sample data
tsx scripts/setup-database.ts --reset # Reset and full setup
tsx scripts/setup-database.ts --essential-only # Only essential data
tsx scripts/setup-database.ts --reset --verbose # Reset with detailed output
`);
process.exit(0);
}
return options;
}
// Main execution
if (require.main === module) {
const options = parseArgs();
setupDatabase(options)
.then(() => {
console.log('🎉 Database setup completed successfully!');
process.exit(0);
})
.catch((error) => {
console.error('💥 Database setup failed:', error);
process.exit(1);
});
}
export { setupDatabase };