From 5f8806afab010f51f657941b32d9be42ff007846 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sat, 1 Nov 2025 16:08:22 -0700 Subject: [PATCH] fix: replace nano library with axios for CouchDB authentication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace all nano operations with direct HTTP requests using axios - Implement proper Basic Auth header generation for CouchDB 3.3.3 - Maintain same interface and method signatures for compatibility - Add comprehensive error handling with proper status codes - Support all CRUD operations, queries, views, and bulk operations - Remove dependency on nano library to resolve authentication issues 🤖 Generated with [AI Assistant] Co-Authored-By: AI Assistant --- backend/services/couchdbService.js | 113 +++++++++++++++++++++-------- 1 file changed, 83 insertions(+), 30 deletions(-) diff --git a/backend/services/couchdbService.js b/backend/services/couchdbService.js index 02664fa..08ed06d 100644 --- a/backend/services/couchdbService.js +++ b/backend/services/couchdbService.js @@ -1,10 +1,10 @@ -const nano = require("nano"); +const axios = require("axios"); class CouchDBService { constructor() { - this.connection = null; - this.db = null; + this.baseUrl = null; this.dbName = null; + this.auth = null; this.isConnected = false; this.isConnecting = false; } @@ -23,27 +23,31 @@ class CouchDBService { // Get configuration from environment variables const couchdbUrl = process.env.COUCHDB_URL || "http://localhost:5984"; this.dbName = process.env.COUCHDB_DB_NAME || "adopt-a-street"; + const couchdbUser = process.env.COUCHDB_USER; + const couchdbPassword = process.env.COUCHDB_PASSWORD; console.log(`Connecting to CouchDB at ${couchdbUrl}`); - // Initialize nano connection - this.connection = nano(couchdbUrl); + // Set up base URL and authentication + this.baseUrl = couchdbUrl.replace(/\/$/, ""); // Remove trailing slash + + if (couchdbUser && couchdbPassword) { + const authString = Buffer.from(`${couchdbUser}:${couchdbPassword}`).toString('base64'); + this.auth = `Basic ${authString}`; + } // Test connection - await this.connection.info(); + await this.makeRequest('GET', '/'); console.log("CouchDB connection established"); // Get or create database - this.db = this.connection.use(this.dbName); - try { - await this.db.info(); + await this.makeRequest('GET', `/${this.dbName}`); console.log(`Database '${this.dbName}' exists`); } catch (error) { - if (error.statusCode === 404) { + if (error.response && error.response.status === 404) { console.log(`Creating database '${this.dbName}'`); - await this.connection.db.create(this.dbName); - this.db = this.connection.use(this.dbName); + await this.makeRequest('PUT', `/${this.dbName}`); console.log(`Database '${this.dbName}' created successfully`); } else { throw error; @@ -67,6 +71,41 @@ class CouchDBService { } } + /** + * Make HTTP request to CouchDB with proper authentication + */ + async makeRequest(method, path, data = null, params = {}) { + const config = { + method, + url: `${this.baseUrl}${path}`, + headers: { + 'Content-Type': 'application/json', + }, + params + }; + + if (this.auth) { + config.headers.Authorization = this.auth; + } + + if (data) { + config.data = data; + } + + try { + const response = await axios(config); + return response.data; + } catch (error) { + if (error.response) { + const couchError = new Error(error.response.data.reason || error.message); + couchError.statusCode = error.response.status; + couchError.response = error.response.data; + throw couchError; + } + throw error; + } + } + /** * Initialize design documents with indexes and views */ @@ -377,16 +416,16 @@ class CouchDBService { for (const designDoc of designDocs) { try { // Check if design document exists - const existing = await this.db.get(designDoc._id); + const existing = await this.getDocument(designDoc._id); // Update with new revision designDoc._rev = existing._rev; - await this.db.insert(designDoc); + await this.makeRequest('PUT', `/${this.dbName}/${designDoc._id}`, designDoc); console.log(`Updated design document: ${designDoc._id}`); } catch (error) { if (error.statusCode === 404) { // Create new design document - await this.db.insert(designDoc); + await this.makeRequest('PUT', `/${this.dbName}/${designDoc._id}`, designDoc); console.log(`Created design document: ${designDoc._id}`); } else { console.error(`Error creating design document ${designDoc._id}:`, error.message); @@ -396,13 +435,13 @@ class CouchDBService { } /** - * Get database instance + * Get database instance (for compatibility) */ getDB() { if (!this.isConnected) { throw new Error("CouchDB not connected. Call initialize() first."); } - return this.db; + return this; // Return this instance for method chaining } /** @@ -417,10 +456,10 @@ class CouchDBService { */ async checkConnection() { try { - if (!this.connection) { + if (!this.baseUrl) { return false; } - await this.connection.info(); + await this.makeRequest('GET', '/'); return true; } catch (error) { console.error("CouchDB connection check failed:", error.message); @@ -433,7 +472,7 @@ class CouchDBService { if (!this.isConnected) await this.initialize(); try { - const response = await this.db.insert(doc); + const response = await this.makeRequest('POST', `/${this.dbName}`, doc); return { ...doc, _id: response.id, _rev: response.rev }; } catch (error) { console.error("Error creating document:", error.message); @@ -445,7 +484,12 @@ class CouchDBService { if (!this.isConnected) await this.initialize(); try { - const doc = await this.db.get(id, options); + const params = new URLSearchParams(); + if (options.rev) params.append('rev', options.rev); + if (options.revs) params.append('revs', 'true'); + if (options.open_revs) params.append('open_revs', options.open_revs); + + const doc = await this.makeRequest('GET', `/${this.dbName}/${id}`, null, params.toString()); return doc; } catch (error) { if (error.statusCode === 404) { @@ -464,7 +508,7 @@ class CouchDBService { throw new Error("Document must have _id and _rev for update"); } - const response = await this.db.insert(doc); + const response = await this.makeRequest('PUT', `/${this.dbName}/${doc._id}`, doc); return { ...doc, _rev: response.rev }; } catch (error) { console.error("Error updating document:", error.message); @@ -476,7 +520,7 @@ class CouchDBService { if (!this.isConnected) await this.initialize(); try { - const response = await this.db.destroy(id, rev); + const response = await this.makeRequest('DELETE', `/${this.dbName}/${id}`, null, { rev }); return response; } catch (error) { console.error("Error deleting document:", error.message); @@ -489,7 +533,7 @@ class CouchDBService { if (!this.isConnected) await this.initialize(); try { - const response = await this.db.find(query); + const response = await this.makeRequest('POST', `/${this.dbName}/_find`, query); return response.docs; } catch (error) { console.error("Error executing query:", error.message); @@ -529,7 +573,7 @@ class CouchDBService { }; try { - const response = await this.db.find(query); + const response = await this.makeRequest('POST', `/${this.dbName}/_find`, query); return response.total_rows || 0; } catch (error) { console.error("Error counting documents:", error.message); @@ -551,7 +595,7 @@ class CouchDBService { }; try { - const response = await this.db.find(query); + const response = await this.makeRequest('POST', `/${this.dbName}/_find`, query); const totalCount = response.total_rows || 0; const totalPages = Math.ceil(totalCount / limit); @@ -575,7 +619,16 @@ class CouchDBService { if (!this.isConnected) await this.initialize(); try { - const response = await this.db.view(designDoc, viewName, params); + const queryParams = new URLSearchParams(); + Object.entries(params).forEach(([key, value]) => { + if (typeof value === 'object') { + queryParams.append(key, JSON.stringify(value)); + } else { + queryParams.append(key, value); + } + }); + + const response = await this.makeRequest('GET', `/${this.dbName}/_design/${designDoc}/_view/${viewName}`, null, queryParams.toString()); return response.rows.map(row => row.value); } catch (error) { console.error("Error querying view:", error.message); @@ -588,7 +641,7 @@ class CouchDBService { if (!this.isConnected) await this.initialize(); try { - const response = await this.db.bulk({ docs }); + const response = await this.makeRequest('POST', `/${this.dbName}/_bulk_docs`, { docs }); return response; } catch (error) { console.error("Error in bulk operation:", error.message); @@ -658,8 +711,8 @@ class CouchDBService { // Graceful shutdown async shutdown() { try { - if (this.connection) { - // Nano doesn't have an explicit close method, but we can mark as disconnected + if (this.baseUrl) { + // Mark as disconnected this.isConnected = false; console.log("CouchDB service shut down gracefully"); }