5.1.4 • Published 3 months ago

hide-a-bed v5.1.4

Weekly downloads
-
License
ISC
Repository
github
Last release
3 months ago

API Quick Reference

šŸ­ denotes a Sugar api - helps makes some tasks sweet and easy, but may hide some complexities you might want to deal with.

Document OperationsBulk OperationsView OperationsChanges Feed
get()bulkGet()query()changes()
put()bulkSave()queryStream()watchDocs() šŸ­
patch() šŸ­bulkRemove()
patchDangerously() šŸ­bulkGetDictionary() šŸ­
getAtRev() šŸ­bulkSaveTransaction() šŸ­
createLock() šŸ­
removeLock() šŸ­

And some utility apis

Setup

Depending on your environment, use import or require import { get, put, query } from 'hide-a-bed' or const { get, put, query } = require('hide-a-bed')

Config

Anywhere you see a config, it is an object with the following setup { couch: 'https://username:pass@the.couch.url.com:5984' } And it is passed in as the first argument of all the functions const doc = await get(config, 'doc-123')

See Advanced Config Options for more advanced settings.

bindConfig

A convience method to bind the config, so you dont need to pass it in.

import { bindConfig } from 'hide-a-bed'
const db = bindConfig(process.env)
const services = { db } // see example below
const doc = await db.get('doc-123')

If you need to force autocompletion/type-checking, you can add the following jsdoc to help your editor

function doSomething (services)
  /**  @type { import('hide-a-bed/index.mjs').DB} db */
  const db = services.db;

Here is an example of compiler warnings:

jsdoc type def

Config Overrides

You also can quickly change one (or more) config settings for a particular call with the db.config(options)

eg

const doc = await db.config({ throwOnGetNotFound: true }).get('doc-id') 

You can pass any of Config Options to db.config to override the original bindings.

Document Operations

get

Get a single document by ID.

Parameters:

  • config: Object with couch URL string and optional throwOnGetNotFound flag
  • id: Document ID string
  • config: Object with
    • couch URL string
    • throwOnGetNotFound default false. If true, 404 docs throw
  • id: Document ID string
  • Returns: Promise resolving to document object or null if not found
const config = { couch: 'http://localhost:5984/mydb' }
const doc = await get(config, 'doc-123')
console.log(doc._id, doc._rev)

const notThereIsNull = await get(config, 'does-not-exist')
console.log(notThereIsNull) // null 

try {
  const config = { couch: '', throwOnGetNotFound: true }
  await get(config, 'does-not-exist')
} catch (err) {
  if (err.name === 'NotFoundError') console.log('Document not found')
}

put

Save a document.

Parameters:

  • config: Object with couch URL string
  • doc: Document object with _id property
  • Returns: Promise resolving to response with ok, id, rev properties
const config = { couch: 'http://localhost:5984/mydb' }
const doc = { 
  _id: 'doc-123',
  type: 'user',
  name: 'Alice'
}
const result = await put(config, doc)
// result: { ok: true, id: 'doc-123', rev: '1-abc123' }

// imaginary rev returns a conflict
const doc = { _id: 'notThereDoc', _rev: '32-does-not-compute'}
const result2 = await db.put(doc)
console.log(result2) // { ok: false, error: 'conflict', statusCode: 409 }

patch

Update specific properties of a document, you must know the _rev, and passed in with properties.

Parameters:

  • config: Object with couch URL string
  • id: Document ID string
  • properties: Object with properties to update, one must be the current _rev
  • Returns: Promise resolving to response with ok, id, rev properties
const config = { 
  couch: 'http://localhost:5984/mydb',
  retries: 3,
  delay: 500
}
const properties = { 
  _rev: '3-fdskjhfsdkjhfsd',
  name: 'Alice Smith',
  updated: true
}
const result = await patch(config, 'doc-123', properties)
// result: { ok: true, id: 'doc-123', rev: '2-xyz789' }

patchDangerously

Update specific properties of a document, no _rev is needed.

Parameters:

  • config: Object with couch URL string
  • id: Document ID string
  • properties: Object with properties to update

warning - this can clobber data. It will retry even if a conflict happens. There are some use cases for this, but you have been warned, hence the name.

  • id: Document ID string
  • properties: Object with properties to update
  • Returns: Promise resolving to response with ok, id, rev properties
const config = { 
  couch: 'http://localhost:5984/mydb',
  retries: 3,
  delay: 500
}
const properties = { 
  name: 'Alice Smith',
  updated: true
}
const result = await patchDangerously(config, 'doc-123', properties)
// result: { ok: true, id: 'doc-123', rev: '2-xyz789' }

getAtRev

Return a document at the rev specified.

Parameters:

  • config: Object with couch URL string
  • id: Document ID string
  • rev: Revision string to retrieve

CouchDB is not a version control db. This is a special function for unique situations. The _rev might not be around as couch cleans up old revs.

const config = { couch: 'http://localhost:5984/mydb' }
const doc = await getAtRev(config, 'doc-123', '2-fsdjfsdakljfsajlksd')
console.log(doc._id, doc._rev)

createLock

Create a lock document to try and prevent concurrent modifications.

Note this does not internally lock the document that is referenced by the id. People can still mutate it with all the other document mutation functions. This should just be used at an app level to coordinate access on long running document editing.

Parameters:

  • config: Object with couch URL string
  • docId: Document ID string to lock
  • options: Lock options object:
    • enableLocking: Boolean to enable/disable locking (default: true)
    • username: String identifying who created the lock

Returns a Promise resolving to boolean indicating if lock was created successfully.

removeLock

Remove a lock from a document.

Parameters:

  • config: Object with couch URL string
  • docId: Document ID string to unlock
  • options: Lock options object:
    • enableLocking: Boolean to enable/disable locking (default: true)
    • username: String identifying who is removing the lock

Only the user who created the lock can remove it.

const config = { couch: 'http://localhost:5984/mydb' }
const options = {
  enableLocking: true,
  username: 'alice'
}

const locked = await createLock(config, 'doc-123', options)
if (locked) {
  // Document is now locked for exclusive access
  // Perform your updates here
  await removeLock(config, 'doc-123', options)
}
// Lock is now removed if it existed and was owned by 'alice'

Bulk Operations

bulkSave

Save multiple documents in one request.

Parameters:

  • config: Object with couch URL string
  • docs: Array of document objects, each with _id
  • Returns: Promise resolving to array of results with ok, id, rev for each doc
const config = { couch: 'http://localhost:5984/mydb' }
const docs = [
  { _id: 'doc1', type: 'user', name: 'Alice' },
  { _id: 'doc2', type: 'user', name: 'Bob' }
]
const results = await bulkSave(config, docs)
// [
//   { ok: true, id: 'doc1', rev: '1-abc123' },
//   { ok: true, id: 'doc2', rev: '1-def456' }
// ]

bulkGet

Get multiple documents by ID.

Parameters:

  • config: Object with couch URL string
  • ids: Array of document ID strings
  • Returns: Promise resolving to array of documents

Not found documents will still have a row in the results, but the doc will be null, and the error property will be set

const config = { couch: 'http://localhost:5984/mydb' }
const ids = ['doc1', 'doc2', 'doesNotExist']
const docs = await bulkGet(config, ids)
// rows: [
//   { _id: 'doc1', _rev: '1-abc123', type: 'user', name: 'Alice' },
//   { _id: 'doc2', _rev: '1-def456', type: 'user', name: 'Bob' },
//   { key: 'notThereDoc', error: 'not_found' }
// ]

bulkRemove

Delete multiple documents in one request.

Parameters:

  • config: Object with couch URL string
  • ids: Array of document ID strings to delete
  • Returns: Promise resolving to array of results with ok, id, rev for each deletion
const config = { couch: 'http://localhost:5984/mydb' }
const ids = ['doc1', 'doc2']
const results = await bulkRemove(config, ids)
// results: [
//   { ok: true, id: 'doc1', rev: '2-ghi789' },
//   { ok: true, id: 'doc2', rev: '2-jkl012' }
// ]

bulkGetDictionary

Adds some convenience to bulkGet. Organizes found and notFound documents into properties that are {id:result}. This makes it easy to deal with the results.

Parameters:

  • config: Object with couch URL string
  • ids: Array of document ID strings to delete
  • Returns: Promise resolving to an object with found and notFound properties.

found looks like

{ 
  id1: { _id: 'id1', _rev: '1-221', data: {} },
  id2: { _id: 'id2', _rev: '4-421', data: {} },
}

notFound looks like

{
  id3: { key: 'id1', error: 'not_found' }
}
const config = { couch: 'http://localhost:5984/mydb' }
const ids = ['doc1', 'doc2']
const results = await bulkGetDictionary(config, ids)
// results: {
//   found: {
//     id1: { _id: 'id1', _rev: '1-221', data: {} },
//     id2: { _id: 'id2', _rev: '4-421', data: {} },
//   },
//   notFound: {
//      id3: { key: 'id1', error: 'not_found' }
//   }
// }

bulkSaveTransaction

Perform a bulk save operation with all-or-nothing semantics.

Parameters:

  • config: Object with couch URL string
  • transactionId: Unique identifier for the transaction
  • docs: Array of document objects to save
  • Returns: Promise resolving to array of results with ok, id, rev for each doc

This operation ensures that either all documents are saved successfully, or none are, maintaining data consistency. If any document fails to save, the operation will attempt to roll back all changes.

Note: The transactionId has to be unique for the lifetime of the app. It is used to prevent two processes from executing the same transaction. It is up to you to craft a transactionId that uniquely represents this transaction, and that also is the same if another process tries to generate it.

Exceptions to handle:

  • TransactionSetupError: Thrown if the transaction document cannot be created. Usually because it already exists
  • TransactionVersionConflictError: Thrown if there are version conflicts with existing documents.
  • TransactionBulkOperationError: Thrown if the bulk save operation fails for some documents.
  • TransactionRollbackError: Thrown if the rollback operation fails after a transaction failure.
const config = { couch: 'http://localhost:5984/mydb' }
const transactionId = 'txn-123'
const docs = [
  { _id: 'doc1', type: 'user', name: 'Alice', _rev: '1-abc123' },
  { _id: 'doc2', type: 'user', name: 'Bob', _rev: '1-def456' }
]

try {
  const results = await bulkSaveTransaction(config, transactionId, docs)
  console.log('Transaction successful:', results)
} catch (error) {
  if (error instanceof TransactionSetupError) {
    // the transaction could not start - usually an existing transaction with the same id
    console.error('Transaction setup failed:', error)
  } else if (error instanceof TransactionVersionConflictError) {
    // one or more of the versions of the docs provided dont match with what is currently in the db
    console.error('Version conflict error:', error)
  } else if (error instanceof TransactionRollbackError) {
    // the transaction was rolled back - so the 'or none' condition occured
    console.error('Rollback error:', error)
  } else {
    console.error('Unexpected error:', error)
  }
}

View Queries

query

Query a view with options.

Parameters:

  • config: Object with couch URL string
  • view: View path string (e.g. '_design/doc/_view/name')
  • options: Optional object with query parameters:
    • startkey: Start key for range
    • endkey: End key for range
    • key: Exact key match
    • descending: Boolean to reverse sort
    • skip: Number of results to skip
    • limit: Max number of results
    • include_docs: Boolean to include full docs
    • reduce: Boolean to reduce results
    • group: Boolean to group results
    • group_level: Number for group level
  • Returns: Promise resolving to response with rows array
const config = { couch: 'http://localhost:5984/mydb' }
const view = '_design/users/_view/by_name'
const options = {
  startkey: 'A',
  endkey: 'B',
  include_docs: true,
  limit: 10
}
const result = await query(config, view, options)
// result: {
//   rows: [
//     { 
//       id: 'doc1',
//       key: 'Alice',
//       value: 1,
//       doc: { _id: 'doc1', name: 'Alice', type: 'user' }
//     },
//     // ... more rows
//   ]
// }

Some notes on the keys. Use native js things for arrays keys, rather then strings. Eg

  • { startkey: ['ryan'], endkey: ['ryan', {}] }
  • { startkey: [47, null], endkey: [48, null] }
  • { startkey: [customerIdVar], endkey: [customerIdVar, {}] }
  • { startkey: [teamId, userId, startTimestamp], endkey: [teamId, userId, endTimestamp] }

createQuery()

Create a query builder to help construct view queries with a fluent interface. Note we have stuck to couch naming conventions and not camel case.

  • Returns: QueryBuilder instance with methods:
    • key(value): Set exact key match
    • startkey(value): Set range start key
    • endkey(value): Set range end key
    • descending(bool): Set descending sort order
    • skip(number): Set number of results to skip
    • limit(number): Set max number of results
    • include_docs(bool): Include full documents
    • reduce(bool): Enable/disable reduce
    • group(bool): Enable/disable grouping
    • group_level(number): Set group level
    • build(): Return the constructed query options object
const options = createQuery()
  .startkey('A')
  .endkey('B')
  .include_docs(true)
  .limit(10)
  .build()

const result = await query(config, view, options)

Again, use js types for array keys

  • .startkey([teamId, userId]).endkey([teamId, userId, {}])
  • .startkey([teamId, userId, startTimestamp]).endkey([teamId, userId, endTimestamp])

queryStream

Use Cases Streaming Data

Parameters:

  • config: Object with couch URL string
  • view: View path string
  • options: Query options object
  • onRow: Function called for each row in the results

Want to stream data from couch? You can with queryStream. It looks identical to query, except you add an extra 'onRow' function

Here is a small hapi example of streaming data from couch to the client as ndjson. We do a small transform by only streaming the doc. you can do a lot of things in the onrow function.

import Hapi from '@hapi/hapi';
import { Readable } from 'stream';
import { queryStream } from bindConfig(process.env)
const view = '_design/users/_view/by_name'

const init = async () => {
  const server = Hapi.server({ port: 3000 })
  server.route({
    method: 'GET',
    path: '/stream',
    handler: async (req, h) => {
      const stream = new Readable({ read() {} });
      const onRow = ({id, key, value, doc}) => stream.push(JSON.stringify(doc) + '\n')
      const options = { startkey: req.query.startLetter, endkey: req.query.startLetter + '|', include_docs: true}
      await queryStream(view, options, onRow)
      stream.push(null) // end stream
      return h.response(stream).type('application/x-ndjson');
    }
  })

  await server.start();
  console.log(`Server running on ${server.info.uri}`);
}
init()

Want to consume this in the browser? I'd recomment https://www.npmjs.com/package/ndjson-readablestream here is a react component that consumes it https://github.com/Azure-Samples/azure-search-openai-demo/pull/532/files#diff-506debba46b93087dc46a916384e56392808bcc02a99d9291557f3e674d4ad6c

changes()

Subscribe to the CouchDB changes feed to receive real-time updates.

Parameters:

  • config: Object with couch URL string
  • 'onChange': function called for each change
  • options: Optional object with parameters:
    • since: String or number indicating where to start from ('now' or update sequence number)
    • include_docs: Boolean to include full documents
    • filter: String name of design document filter function
    • Other standard CouchDB changes feed parameters

Returns an EventEmitter that emits 'change' events with change objects.

const config = { couch: 'http://localhost:5984/mydb' }
const options = { 
  since: 'now',
  include_docs: true
}

const onChange = change => {
  console.log('Document changed:', change.id)
  console.log('New revision:', change.changes[0].rev)
  if (change.doc) {
    console.log('Document contents:', change.doc)
  }
}
const feed = await changes(config, onChange, options)


// Stop listening to changes
feed.stop()

The changes feed is useful for:

  • Building real-time applications
  • Keeping local data in sync with CouchDB
  • Triggering actions when documents change
  • Implementing replication

watchDocs()

Watch specific documents for changes in real-time.

Parameters:

  • config: Object with couch URL string
  • docIds: String or array of document IDs to watch (max 100
  • onChange: Function called for each change
  • options: Optional object with parameters:

    • include_docs: Boolean to include full documents (defaul false)
    • maxRetries: Maximum reconnection attempts (default: 10)
    • initialDelay: Initial reconnection delay in ms (default 1000)
    • maxDelay: Maximum reconnection delay in ms (default: 30000)

    Returns an EventEmitter that emits:

  • 'change' events with change objects

  • 'error' events when max retries reached
  • 'end' events with last sequence number

    const config = { couch: 'http://localhost:5984/mydb' }
    
    // Watch a single document
    const feed = await watchDocs(config, 'doc123', change => {
      console.log('Document changed:', change.id)
      console.log('New revision:', change.changes[0].rev)
    })
    
    // Watch multiple documents with full doc content
    const feed = await watchDocs(
      config,
      ['doc1', 'doc2', 'doc3'],
      change => {
        if (change.doc) {
          console.log('Updated document:', change.doc)
        }
      },
      { include_docs: true }
    )
    
    // Handle errors
    feed.on('error', error => {
      console.error('Watch error:', error)
    })
    
    // Handle end of feed
    feed.on('end', ({ lastSeq }) => {
      console.log('Feed ended at sequence:', lastSeq)
    })
    
    // Stop watching
    feed.stop()

    The watchDocs feed is useful for:

  • Building real-time applications focused on specific documents

  • Triggering actions when particular documents change
  • Maintaining cached copies of frequently accessed documents

Advanced Config Options

The config object supports the following properties:

PropertyTypeDefaultDescription
couchstringrequiredThe URL of the CouchDB database
throwOnGetNotFoundbooleanfalseIf true, throws an error when get() returns 404. If false, returns undefined
bindWithRetrybooleantrueWhen using bindConfig(), adds retry logic to bound methods
maxRetriesnumber3Maximum number of retry attempts for retryable operations
initialDelaynumber1000Initial delay in milliseconds before first retry
backoffFactornumber2Multiplier for exponential backoff between retries
useConsoleLoggerbooleanfalseIf true, enables console logging when no logger is provided
loggerobject/functionundefinedCustom logging interface (winston-style object or function)

Example configuration with all options:

const config = {
  couch: 'http://localhost:5984/mydb',
  throwOnGetNotFound: true,
  bindWithRetry: true,
  maxRetries: 5,
  initialDelay: 2000,
  backoffFactor: 1.5,
  useConsoleLogger: true,
  logger: (level, ...args) => console.log(level, ...args)
}

Logging Support

The library supports flexible logging options that can be configured through the config object:

// Enable console logging (error, warn, info, debug)
const config = { 
  couch: 'http://localhost:5984/mydb',
  useConsoleLogger: true
}

// Use a custom logger object (winston-style)
const config = {
  couch: 'http://localhost:5984/mydb',
  logger: {
    error: (msg) => console.error(msg),
    warn: (msg) => console.warn(msg),
    info: (msg) => console.info(msg),
    debug: (msg) => console.debug(msg)
  }
}

// Use a simple function logger
const config = {
  couch: 'http://localhost:5984/mydb',
  logger: (level, ...args) => console.log(level, ...args)
}

The logger will track operations including:

  • Document operations (get, put, patch)
  • Bulk operations
  • View queries
  • Streaming operations
  • Retries and error handling

Each operation logs appropriate information at these levels:

  • error: Fatal/unrecoverable errors
  • warn: Retryable errors, conflicts
  • info: Operation start/completion
  • debug: Detailed operation information
5.1.4

3 months ago

5.0.5

4 months ago

5.1.3

3 months ago

5.0.4

4 months ago

5.1.2

3 months ago

5.0.3

4 months ago

5.1.1

3 months ago

5.0.2

4 months ago

5.1.0

4 months ago

5.0.0

4 months ago

2.1.0

5 months ago

3.0.1

5 months ago

3.0.0

5 months ago

4.0.1

5 months ago

4.0.0

5 months ago

4.1.2

4 months ago

4.0.3

5 months ago

4.2.0

4 months ago

4.1.1

4 months ago

4.0.2

5 months ago

1.2.0

10 months ago

1.1.1

10 months ago

1.1.0

11 months ago

1.0.0

2 years ago