fix: resolve CouchDB deployment and init job issues

- Remove problematic ERL_FLAGS from CouchDB StatefulSet
- Copy setup-couchdb.js to backend/scripts/ for Docker image inclusion
- Update init job to use full DNS name and add timeout/retry logic
- Fix script path in init job command

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
William Valentin
2025-12-06 14:00:44 -08:00
parent fc23f4d098
commit cde4850650
3 changed files with 515 additions and 12 deletions

View File

@@ -0,0 +1,472 @@
#!/usr/bin/env node
// Setup module path to include backend node_modules
const path = require('path');
const backendPath = path.join(__dirname, '..', 'backend');
process.env.NODE_PATH = path.join(backendPath, 'node_modules') + ':' + (process.env.NODE_PATH || '');
require('module').Module._initPaths();
const Nano = require('nano');
require('dotenv').config({ path: path.join(backendPath, '.env') });
// 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;