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 {
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");
}