#!/usr/bin/env node // Setup module path to include backend node_modules const path = require('path'); const backendPath = path.join(__dirname, '..'); process.env.NODE_PATH = path.join(backendPath, 'node_modules') + ':' + (process.env.NODE_PATH || ''); require('module').Module._initPaths(); const Nano = require('nano'); // Load .env file if it exists (for local development) const dotenvPath = path.join(backendPath, '.env'); const fs = require('fs'); if (fs.existsSync(dotenvPath)) { require('dotenv').config({ path: dotenvPath }); } // Configuration const COUCHDB_URL = process.env.COUCHDB_URL || 'http://localhost:5984'; const COUCHDB_USER = process.env.COUCHDB_USER || 'admin'; const COUCHDB_PASSWORD = process.env.COUCHDB_PASSWORD || 'admin'; const COUCHDB_DB_NAME = process.env.COUCHDB_DB_NAME || 'adopt-a-street'; class CouchDBSetup { constructor() { this.nano = Nano({ url: COUCHDB_URL, auth: { username: COUCHDB_USER, password: COUCHDB_PASSWORD } }); } async initialize() { console.log('šŸš€ Initializing CouchDB setup...'); try { // Test connection await this.nano.info(); console.log('āœ… Connected to CouchDB'); } catch (error) { console.error('āŒ Failed to connect to CouchDB:', error.message); process.exit(1); } } async createDatabase() { console.log(`šŸ“¦ Creating database: ${COUCHDB_DB_NAME}`); try { await this.nano.db.create(COUCHDB_DB_NAME); console.log('āœ… Database created successfully'); } catch (error) { if (error.statusCode === 412) { console.log('ā„¹ļø Database already exists'); } else { console.error('āŒ Failed to create database:', error.message); throw error; } } } async createIndexes() { console.log('šŸ” Creating indexes...'); const db = this.nano.use(COUCHDB_DB_NAME); // Create design document with indexes const designDoc = { _id: '_design/adopt-a-street', views: { users_by_email: { map: "function(doc) { if (doc.type === 'user') { emit(doc.email, doc); } }" }, streets_by_location: { map: "function(doc) { if (doc.type === 'street') { emit(doc.location, doc); } }" }, by_user: { map: "function(doc) { if (doc.user && doc.user.userId) { emit(doc.user.userId, doc); } }" }, users_by_points: { map: "function(doc) { if (doc.type === 'user') { emit(doc.points, doc); } }" }, posts_by_date: { map: "function(doc) { if (doc.type === 'post') { emit(doc.createdAt, doc); } }" }, streets_by_status: { map: "function(doc) { if (doc.type === 'street') { emit(doc.status, doc); } }" }, events_by_date_status: { map: "function(doc) { if (doc.type === 'event') { emit([doc.date, doc.status], doc); } }" }, comments_by_post: { map: "function(doc) { if (doc.type === 'comment' && doc.post && doc.post.postId) { emit(doc.post.postId, doc); } }" }, transactions_by_user_date: { map: "function(doc) { if (doc.type === 'point_transaction' && doc.user && doc.user.userId) { emit([doc.user.userId, doc.createdAt], doc); } }" } }, indexes: { users_by_email: { index: { fields: ["type", "email"] }, name: "user-by-email", type: "json" }, streets_by_location: { index: { fields: ["type", "location"] }, name: "streets-by-location", type: "json" }, by_user: { index: { fields: ["type", "user.userId"] }, name: "by-user", type: "json" }, users_by_points: { index: { fields: ["type", "points"] }, name: "users-by-points", type: "json" }, posts_by_date: { index: { fields: ["type", "createdAt"] }, name: "posts-by-date", type: "json" }, streets_by_status: { index: { fields: ["type", "status"] }, name: "streets-by-status", type: "json" }, events_by_date_status: { index: { fields: ["type", "date", "status"] }, name: "events-by-date-status", type: "json" }, comments_by_post: { index: { fields: ["type", "post.postId"] }, name: "comments-by-post", type: "json" }, transactions_by_user_date: { index: { fields: ["type", "user.userId", "createdAt"] }, name: "transactions-by-user-date", type: "json" } }, language: "javascript" }; try { await db.insert(designDoc); console.log('āœ… Design document and indexes created successfully'); } catch (error) { if (error.statusCode === 409) { // Document already exists, update it const existing = await db.get('_design/adopt-a-street'); designDoc._rev = existing._rev; await db.insert(designDoc); console.log('āœ… Design document and indexes updated successfully'); } else { console.error('āŒ Failed to create indexes:', error.message); throw error; } } } async createSecurityDocument() { console.log('šŸ”’ Setting up security document...'); const db = this.nano.use(COUCHDB_DB_NAME); const securityDoc = { admins: { names: [COUCHDB_USER], roles: [] }, members: { names: [], roles: [] } }; try { await db.insert(securityDoc, '_security'); console.log('āœ… Security document created successfully'); } catch (error) { console.warn('āš ļø Failed to create security document:', error.message); } } async seedBadges() { console.log('šŸ† Seeding badges...'); const db = this.nano.use(COUCHDB_DB_NAME); // Check if badges already exist try { const existingBadges = await db.find({ selector: { type: 'badge' }, limit: 1 }); if (existingBadges.docs.length > 0) { console.log('ā„¹ļø Badges already exist, skipping seeding'); return; } } catch (error) { // Continue with seeding } const badges = [ // Street Adoption Badges { _id: 'badge_first_adoption', type: 'badge', name: 'First Adoption', description: 'Adopted your first street', icon: 'šŸ”', criteria: { type: 'street_adoptions', threshold: 1 }, rarity: 'common', order: 1, isActive: true, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }, { _id: 'badge_street_adopter', type: 'badge', name: 'Street Adopter', description: 'Adopted 5 streets', icon: 'šŸ˜ļø', criteria: { type: 'street_adoptions', threshold: 5 }, rarity: 'rare', order: 2, isActive: true, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }, { _id: 'badge_neighborhood_champion', type: 'badge', name: 'Neighborhood Champion', description: 'Adopted 10 streets', icon: 'šŸŒ†', criteria: { type: 'street_adoptions', threshold: 10 }, rarity: 'epic', order: 3, isActive: true, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }, { _id: 'badge_city_guardian', type: 'badge', name: 'City Guardian', description: 'Adopted 25 streets', icon: 'šŸ™ļø', criteria: { type: 'street_adoptions', threshold: 25 }, rarity: 'legendary', order: 4, isActive: true, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }, // Task Completion Badges { _id: 'badge_first_task', type: 'badge', name: 'First Task', description: 'Completed your first task', icon: 'āœ…', criteria: { type: 'task_completions', threshold: 1 }, rarity: 'common', order: 5, isActive: true, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }, { _id: 'badge_task_master', type: 'badge', name: 'Task Master', description: 'Completed 10 tasks', icon: 'šŸŽÆ', criteria: { type: 'task_completions', threshold: 10 }, rarity: 'rare', order: 6, isActive: true, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }, { _id: 'badge_dedicated_worker', type: 'badge', name: 'Dedicated Worker', description: 'Completed 50 tasks', icon: 'šŸ› ļø', criteria: { type: 'task_completions', threshold: 50 }, rarity: 'epic', order: 7, isActive: true, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }, { _id: 'badge_maintenance_legend', type: 'badge', name: 'Maintenance Legend', description: 'Completed 100 tasks', icon: '⚔', criteria: { type: 'task_completions', threshold: 100 }, rarity: 'legendary', order: 8, isActive: true, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } ]; try { const results = await db.bulk({ docs: badges }); const successCount = results.filter(r => !r.error).length; console.log(`āœ… Successfully seeded ${successCount} badges`); } catch (error) { console.error('āŒ Failed to seed badges:', error.message); throw error; } } async verifySetup() { console.log('šŸ” Verifying setup...'); const db = this.nano.use(COUCHDB_DB_NAME); try { // Check database info const info = await db.info(); console.log(`āœ… Database "${info.db_name}" is ready`); console.log(` - Doc count: ${info.doc_count}`); console.log(` - Update seq: ${info.update_seq}`); // Check indexes const designDoc = await db.get('_design/adopt-a-street'); const indexCount = Object.keys(designDoc.indexes || {}).length; console.log(`āœ… ${indexCount} indexes created`); // Check badges const badges = await db.find({ selector: { type: 'badge' }, fields: ['name'] }); console.log(`āœ… ${badges.docs.length} badges available`); } catch (error) { console.error('āŒ Setup verification failed:', error.message); throw error; } } async seedAdminUser() { console.log('šŸ‘¤ Seeding admin user...'); const ADMIN_EMAIL = process.env.ADMIN_EMAIL; const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD; if (!ADMIN_EMAIL || !ADMIN_PASSWORD) { console.log('āš ļø ADMIN_EMAIL or ADMIN_PASSWORD not set, skipping admin user creation'); return; } const db = this.nano.use(COUCHDB_DB_NAME); // Check if admin user already exists try { const existing = await db.find({ selector: { type: 'user', email: ADMIN_EMAIL }, limit: 1 }); if (existing.docs.length > 0) { const user = existing.docs[0]; if (!user.isAdmin) { user.isAdmin = true; user.updatedAt = new Date().toISOString(); await db.insert(user); console.log('āœ… Existing user promoted to admin'); } else { console.log('ā„¹ļø Admin user already exists'); } return; } } catch (error) { // Continue with creation } // Create new admin user with hashed password const bcrypt = require('bcryptjs'); const salt = await bcrypt.genSalt(10); const hashedPassword = await bcrypt.hash(ADMIN_PASSWORD, salt); const adminUser = { _id: `user_admin_${Date.now()}`, type: 'user', name: 'Administrator', email: ADMIN_EMAIL, password: hashedPassword, isAdmin: true, isPremium: true, points: 0, avatar: null, profilePicture: null, bio: 'System Administrator', location: '', website: '', social: { twitter: '', github: '', linkedin: '' }, privacySettings: { profileVisibility: 'private' }, preferences: { emailNotifications: true, pushNotifications: true, theme: 'light' }, adoptedStreets: [], completedTasks: [], posts: [], events: [], earnedBadges: [], stats: { streetsAdopted: 0, tasksCompleted: 0, postsCreated: 0, eventsParticipated: 0, badgesEarned: 0 }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; await db.insert(adminUser); console.log(`āœ… Admin user created: ${ADMIN_EMAIL}`); } async run() { try { await this.initialize(); await this.createDatabase(); await this.createIndexes(); await this.createSecurityDocument(); await this.seedBadges(); await this.seedAdminUser(); await this.verifySetup(); console.log('\nšŸŽ‰ CouchDB setup completed successfully!'); console.log(`\nšŸ“‹ Connection Details:`); console.log(` URL: ${COUCHDB_URL}`); console.log(` Database: ${COUCHDB_DB_NAME}`); console.log(` User: ${COUCHDB_USER}`); console.log(`\n🌐 Access CouchDB at: ${COUCHDB_URL}/_utils`); } catch (error) { console.error('\nāŒ Setup failed:', error.message); process.exit(1); } } } // Run setup if called directly if (require.main === module) { const setup = new CouchDBSetup(); setup.run().catch(console.error); } module.exports = CouchDBSetup;