@debito/hippo-lib
Version:
Double-entry accounting library for CouchDB
322 lines (288 loc) • 10.6 kB
JavaScript
import axios from 'axios';
/**
* CouchDB Driver using axios for universal browser/Node.js compatibility
* Provides generic CRUD operations and CouchDB-specific functionality
*/
class CouchDBDriver {
baseURL;
username;
password;
timeout;
defaultHeaders;
database;
/**
* Initialize CouchDB driver
* @param {Object} config - Database configuration
* @param {string} config.url - CouchDB server URL
* @param {string} config.username - Database username
* @param {string} config.password - Database password
* @param {string} config.activeDatabase - Default database name (optional)
* @param {number} config.timeout - Request timeout in milliseconds
*/
constructor(config) {
this.baseURL = config.url;
this.username = config.username;
this.password = config.password;
this.database = config.activeDatabase;
this.timeout = config.timeout || 10000;
this.defaultHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json'
};
// Configure axios instance
this.client = axios.create({
baseURL: this.baseURL,
timeout: this.timeout,
headers: this.defaultHeaders,
auth: this.username && this.password ? {
username: this.username,
password: this.password
} : undefined
});
// Add response interceptor for error handling
this.client.interceptors.response.use(
response => response,
error => {
if (error.response) {
// CouchDB error response
const couchError = new Error(error.response.data.reason || error.response.data.error || 'CouchDB Error');
couchError.status = error.response.status;
couchError.error = error.response.data.error;
couchError.reason = error.response.data.reason;
throw couchError;
} else if (error.request) {
// Network error
throw new Error('Network error: Unable to connect to CouchDB');
} else {
// Other error
throw error;
}
}
);
}
/**
* Initialize and validate the driver (call this after constructor if activeDatabase is set)
* @returns {Promise<void>}
*/
async initialize() {
if (this.dbName) {
await this._validateDatabase();
}
}
/**
* Validate that the database exists
* @private
*/
async _validateDatabase() {
try {
const exists = await this.databaseExists(this.database);
if (!exists) {
throw new Error(`Database '${this.database}' does not exist`);
}
} catch (error) {
if (error.message.includes('does not exist')) {
throw error;
}
throw new Error(`Failed to validate database '${this.database}': ${error.message}`);
}
}
/**
* Get server information
* @returns {Promise<Object>} Server info
*/
async serverInfo() {
const response = await this.client.get('/');
return response.data;
}
/**
* Create a database
* @param {string} dbName - Database name
* @returns {Promise<Object>} Creation result
*/
async createDatabase(dbName) {
const response = await this.client.put(`/${dbName}`);
return response.data;
}
/**
* Delete a database
* @param {string} dbName - Database name
* @returns {Promise<Object>} Deletion result
*/
async deleteDatabase(dbName) {
const response = await this.client.delete(`/${dbName}`);
return response.data;
}
/**
* Check if database exists
* @param {string} dbName - Database name
* @returns {Promise<boolean>} Database exists
*/
async databaseExists(dbName) {
try {
await this.client.head(`/${dbName}`);
return true;
} catch (error) {
if (error.status === 404) {
return false;
}
throw error;
}
}
/**
* Get database information
* @param {string} dbName - Database name
* @returns {Promise<Object>} Database info
*/
async databaseInfo() {
if (!this.database) throw new Error('No database specified');
const response = await this.client.get(`/${this.database}`);
return response.data;
}
/**
* Create or update a document (nano-compatible: insert)
* @param {Object} doc - Document data
* @param {string} docId - Document ID (optional, auto-generated if not provided)
* @returns {Promise<Object>} Creation/update result
*/
async insert(doc, docId = null) {
if (!this.database) throw new Error('No database specified');
if (docId) {
const response = await this.client.put(`/${this.database}/${docId}`, doc);
return response.data;
} else {
const response = await this.client.post(`/${this.database}`, doc);
return response.data;
}
}
/**
* Get a document by ID (nano-compatible: get)
* @param {string} docId - Document ID
* @param {Object} options - Query options (rev, revs, etc.)
* @returns {Promise<Object>} Document data
*/
async get(docId, options = {}) {
if (!this.database) throw new Error('No database specified');
const params = new URLSearchParams(options);
const response = await this.client.get(`/${this.database}/${docId}?${params}`);
return response.data;
}
/**
* Delete a document (nano-compatible: destroy)
* @param {string} docId - Document ID
* @param {string} rev - Document revision
* @returns {Promise<Object>} Deletion result
*/
async destroy(docId, rev) {
if (!this.database) throw new Error('No database specified');
const response = await this.client.delete(`/${this.database}/${docId}?rev=${rev}`);
return response.data;
}
/**
* Bulk operations (nano-compatible: bulk)
* @param {Object} bulkDocs - Bulk documents object
* @returns {Promise<Array>} Results array
*/
async bulk(bulkDocs) {
if (!this.database) throw new Error('No database specified');
const response = await this.client.post(`/${this.database}/_bulk_docs`, bulkDocs);
return response.data;
}
/**
* Get all documents (nano-compatible: list)
* @param {Object} options - Query options
* @returns {Promise<Object>} All documents result
*/
async list(options = {}) {
if (!this.database) throw new Error('No database specified');
// Default options
const _options = {
limit: 10000
};
Object.assign(_options, options);
// Build query string manually for proper CouchDB parameter handling
let queryString = '';
if (_options.startkey) queryString += `startkey="${encodeURIComponent(_options.startkey)}"&`;
if (_options.endkey) queryString += `endkey="${encodeURIComponent(_options.endkey)}"&`;
if (_options.include_docs) queryString += `include_docs=${_options.include_docs}&`;
if (_options.limit) queryString += `limit=${_options.limit}&`;
if (_options.skip) queryString += `skip=${_options.skip}&`;
// Remove trailing &
if (queryString.endsWith('&')) queryString = queryString.slice(0, -1);
const url = `/${this.database}/_all_docs${queryString ? '?' + queryString : ''}`;
const response = await this.client.get(url);
return response.data;
}
/**
* Query a view
* @param {string} designDoc - Design document name
* @param {string} viewName - View name
* @param {Object} options - Query options
* @returns {Promise<Object>} View results
*/
async view(designDoc, viewName, options = {}) {
if (!this.database) throw new Error('No database specified');
const params = new URLSearchParams(options);
const response = await this.client.get(`/${this.database}/_design/${designDoc}/_view/${viewName}?${params}`);
return response.data;
}
/**
* Create or update an index
* @param {Object} indexDef - Index definition
* @returns {Promise<Object>} Index creation result
*/
async createIndex(indexDef) {
if (!this.database) throw new Error('No database specified');
const response = await this.client.post(`/${this.database}/_index`, indexDef);
return response.data;
}
/**
* Find documents using Mango queries
* @param {Object} query - Mango query
* @returns {Promise<Object>} Query results
*/
async find(query) {
if (!this.database) throw new Error('No database specified');
const response = await this.client.post(`/${this.database}/_find`, query);
return response.data;
}
/**
* Get database changes
* @param {Object} options - Changes options
* @returns {Promise<Object>} Changes result
*/
async changes(options = {}) {
if (!this.database) throw new Error('No database specified');
const params = new URLSearchParams(options);
const response = await this.client.get(`/${this.database}/_changes?${params}`);
return response.data;
}
/**
* Get list of all databases
* @returns {Promise<Array>} Database names array
*/
async listDatabases() {
const response = await this.client.get('/_all_dbs');
return response.data;
}
/**
* Execute raw HTTP request for custom operations
* @param {string} method - HTTP method
* @param {string} path - Request path
* @param {Object} data - Request data
* @param {Object} options - Additional options
* @returns {Promise<Object>} Raw response
*/
async raw(method, path, data = null, options = {}) {
const config = {
method: method.toLowerCase(),
url: path,
...options
};
if (data) {
config.data = data;
}
const response = await this.client.request(config);
return response.data;
}
}
export default CouchDBDriver;