fix: replace nano library with axios for CouchDB authentication

- 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 <noreply@ai-assistant.com>
This commit is contained in:
William Valentin
2025-11-01 16:08:22 -07:00
parent b8376d08ce
commit 5f8806afab

View File

@@ -1,10 +1,10 @@
const nano = require("nano"); const axios = require("axios");
class CouchDBService { class CouchDBService {
constructor() { constructor() {
this.connection = null; this.baseUrl = null;
this.db = null;
this.dbName = null; this.dbName = null;
this.auth = null;
this.isConnected = false; this.isConnected = false;
this.isConnecting = false; this.isConnecting = false;
} }
@@ -23,27 +23,31 @@ class CouchDBService {
// Get configuration from environment variables // Get configuration from environment variables
const couchdbUrl = process.env.COUCHDB_URL || "http://localhost:5984"; const couchdbUrl = process.env.COUCHDB_URL || "http://localhost:5984";
this.dbName = process.env.COUCHDB_DB_NAME || "adopt-a-street"; 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}`); console.log(`Connecting to CouchDB at ${couchdbUrl}`);
// Initialize nano connection // Set up base URL and authentication
this.connection = nano(couchdbUrl); this.baseUrl = couchdbUrl.replace(/\/$/, ""); // Remove trailing slash
if (couchdbUser && couchdbPassword) {
const authString = Buffer.from(`${couchdbUser}:${couchdbPassword}`).toString('base64');
this.auth = `Basic ${authString}`;
}
// Test connection // Test connection
await this.connection.info(); await this.makeRequest('GET', '/');
console.log("CouchDB connection established"); console.log("CouchDB connection established");
// Get or create database // Get or create database
this.db = this.connection.use(this.dbName);
try { try {
await this.db.info(); await this.makeRequest('GET', `/${this.dbName}`);
console.log(`Database '${this.dbName}' exists`); console.log(`Database '${this.dbName}' exists`);
} catch (error) { } catch (error) {
if (error.statusCode === 404) { if (error.response && error.response.status === 404) {
console.log(`Creating database '${this.dbName}'`); console.log(`Creating database '${this.dbName}'`);
await this.connection.db.create(this.dbName); await this.makeRequest('PUT', `/${this.dbName}`);
this.db = this.connection.use(this.dbName);
console.log(`Database '${this.dbName}' created successfully`); console.log(`Database '${this.dbName}' created successfully`);
} else { } else {
throw error; 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 * Initialize design documents with indexes and views
*/ */
@@ -377,16 +416,16 @@ class CouchDBService {
for (const designDoc of designDocs) { for (const designDoc of designDocs) {
try { try {
// Check if design document exists // Check if design document exists
const existing = await this.db.get(designDoc._id); const existing = await this.getDocument(designDoc._id);
// Update with new revision // Update with new revision
designDoc._rev = existing._rev; 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}`); console.log(`Updated design document: ${designDoc._id}`);
} catch (error) { } catch (error) {
if (error.statusCode === 404) { if (error.statusCode === 404) {
// Create new design document // 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}`); console.log(`Created design document: ${designDoc._id}`);
} else { } else {
console.error(`Error creating design document ${designDoc._id}:`, error.message); 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() { getDB() {
if (!this.isConnected) { if (!this.isConnected) {
throw new Error("CouchDB not connected. Call initialize() first."); 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() { async checkConnection() {
try { try {
if (!this.connection) { if (!this.baseUrl) {
return false; return false;
} }
await this.connection.info(); await this.makeRequest('GET', '/');
return true; return true;
} catch (error) { } catch (error) {
console.error("CouchDB connection check failed:", error.message); console.error("CouchDB connection check failed:", error.message);
@@ -433,7 +472,7 @@ class CouchDBService {
if (!this.isConnected) await this.initialize(); if (!this.isConnected) await this.initialize();
try { 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 }; return { ...doc, _id: response.id, _rev: response.rev };
} catch (error) { } catch (error) {
console.error("Error creating document:", error.message); console.error("Error creating document:", error.message);
@@ -445,7 +484,12 @@ class CouchDBService {
if (!this.isConnected) await this.initialize(); if (!this.isConnected) await this.initialize();
try { 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; return doc;
} catch (error) { } catch (error) {
if (error.statusCode === 404) { if (error.statusCode === 404) {
@@ -464,7 +508,7 @@ class CouchDBService {
throw new Error("Document must have _id and _rev for update"); 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 }; return { ...doc, _rev: response.rev };
} catch (error) { } catch (error) {
console.error("Error updating document:", error.message); console.error("Error updating document:", error.message);
@@ -476,7 +520,7 @@ class CouchDBService {
if (!this.isConnected) await this.initialize(); if (!this.isConnected) await this.initialize();
try { try {
const response = await this.db.destroy(id, rev); const response = await this.makeRequest('DELETE', `/${this.dbName}/${id}`, null, { rev });
return response; return response;
} catch (error) { } catch (error) {
console.error("Error deleting document:", error.message); console.error("Error deleting document:", error.message);
@@ -489,7 +533,7 @@ class CouchDBService {
if (!this.isConnected) await this.initialize(); if (!this.isConnected) await this.initialize();
try { try {
const response = await this.db.find(query); const response = await this.makeRequest('POST', `/${this.dbName}/_find`, query);
return response.docs; return response.docs;
} catch (error) { } catch (error) {
console.error("Error executing query:", error.message); console.error("Error executing query:", error.message);
@@ -529,7 +573,7 @@ class CouchDBService {
}; };
try { try {
const response = await this.db.find(query); const response = await this.makeRequest('POST', `/${this.dbName}/_find`, query);
return response.total_rows || 0; return response.total_rows || 0;
} catch (error) { } catch (error) {
console.error("Error counting documents:", error.message); console.error("Error counting documents:", error.message);
@@ -551,7 +595,7 @@ class CouchDBService {
}; };
try { 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 totalCount = response.total_rows || 0;
const totalPages = Math.ceil(totalCount / limit); const totalPages = Math.ceil(totalCount / limit);
@@ -575,7 +619,16 @@ class CouchDBService {
if (!this.isConnected) await this.initialize(); if (!this.isConnected) await this.initialize();
try { 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); return response.rows.map(row => row.value);
} catch (error) { } catch (error) {
console.error("Error querying view:", error.message); console.error("Error querying view:", error.message);
@@ -588,7 +641,7 @@ class CouchDBService {
if (!this.isConnected) await this.initialize(); if (!this.isConnected) await this.initialize();
try { try {
const response = await this.db.bulk({ docs }); const response = await this.makeRequest('POST', `/${this.dbName}/_bulk_docs`, { docs });
return response; return response;
} catch (error) { } catch (error) {
console.error("Error in bulk operation:", error.message); console.error("Error in bulk operation:", error.message);
@@ -658,8 +711,8 @@ class CouchDBService {
// Graceful shutdown // Graceful shutdown
async shutdown() { async shutdown() {
try { try {
if (this.connection) { if (this.baseUrl) {
// Nano doesn't have an explicit close method, but we can mark as disconnected // Mark as disconnected
this.isConnected = false; this.isConnected = false;
console.log("CouchDB service shut down gracefully"); console.log("CouchDB service shut down gracefully");
} }