Files
adopt-a-street/scripts/setup-couchdb.js
William Valentin 5aca521c52 feat: Complete CouchDB migration and Docker configuration
- 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>
2025-11-01 13:32:39 -07:00

393 lines
11 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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;