- Add comprehensive CouchDB setup and configuration - Update Docker files for CouchDB compatibility - Create Kubernetes manifests for CouchDB deployment - Add migration scripts and documentation - Update seeding scripts to support both CouchDB and MongoDB - Add docker-compose for local development - Create comprehensive setup and deployment guides 🤖 Generated with [AI Assistant] Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
393 lines
11 KiB
JavaScript
393 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
const Nano = require('nano');
|
||
require('dotenv').config();
|
||
|
||
// 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 run() {
|
||
try {
|
||
await this.initialize();
|
||
await this.createDatabase();
|
||
await this.createIndexes();
|
||
await this.createSecurityDocument();
|
||
await this.seedBadges();
|
||
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; |