UNPKG

@debito/hippo-lib

Version:

Double-entry accounting library for CouchDB

322 lines (288 loc) 10.6 kB
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;