@decaf-ts/for-couchdb
Version:
template for ts projects
1,277 lines (1,266 loc) • 172 kB
JavaScript
import { Repository, PersistenceKeys, pk, index, table, BaseModel, Sequence as Sequence$1, Paginator, PagingError, QueryError, Statement, Condition, GroupOperator, Operator, final, Adapter, ConnectionError } from '@decaf-ts/core';
import { DefaultSeparator, NotFoundError, InternalError, BaseError, findPrimaryKey, prefixMethod, ConflictError } from '@decaf-ts/db-decorators';
import { __decorate, __metadata } from 'tslib';
import { required, model } from '@decaf-ts/decorator-validation';
import 'reflect-metadata';
/**
* @description Regular expression to identify reserved attributes in CouchDB
* @summary Matches any attribute that starts with an underscore
* @const reservedAttributes
* @memberOf module:for-couchdb
*/
const reservedAttributes = /^_.*$/g;
/**
* @description Key constants used in CouchDB operations
* @summary Collection of string constants for CouchDB document properties and operations
* @typedef {Object} CouchDBKeysType
* @property {string} SEPARATOR - Separator used for combining table name and ID
* @property {string} ID - CouchDB document ID field
* @property {string} REV - CouchDB document revision field
* @property {string} DELETED - CouchDB deleted document marker
* @property {string} TABLE - Table name marker
* @property {string} SEQUENCE - Sequence marker
* @property {string} DDOC - Design document marker
* @property {string} NATIVE - Native marker
* @property {string} INDEX - Index marker
* @memberOf module:for-couchdb
*/
/**
* @description Key constants used in CouchDB operations
* @summary Collection of string constants for CouchDB document properties and operations
* @const CouchDBKeys
* @type {CouchDBKeysType}
* @memberOf module:for-couchdb
*/
const CouchDBKeys = {
SEPARATOR: "__",
ID: "_id",
REV: "_rev",
DELETED: "_deleted",
TABLE: "??table",
SEQUENCE: "??sequence",
DDOC: "ddoc",
NATIVE: "__native",
INDEX: "index",
};
/**
* @description Default query limit for CouchDB queries
* @summary Maximum number of documents to return in a single query
* @const CouchDBQueryLimit
* @memberOf module:for-couchdb
*/
const CouchDBQueryLimit = 250;
/**
* @description Mapping of operator names to CouchDB Mango query operators
* @summary Constants for CouchDB comparison operators used in Mango queries
* @typedef {Object} CouchDBOperatorType
* @property {string} EQUAL - Equality operator ($eq)
* @property {string} DIFFERENT - Inequality operator ($ne)
* @property {string} BIGGER - Greater than operator ($gt)
* @property {string} BIGGER_EQ - Greater than or equal operator ($gte)
* @property {string} SMALLER - Less than operator ($lt)
* @property {string} SMALLER_EQ - Less than or equal operator ($lte)
* @property {string} NOT - Negation operator ($not)
* @property {string} IN - In array operator ($in)
* @property {string} REGEXP - Regular expression operator ($regex)
* @const CouchDBOperator
* @type {CouchDBOperatorType}
* @memberOf module:for-couchdb
*/
const CouchDBOperator = {
EQUAL: "$eq",
DIFFERENT: "$ne",
BIGGER: "$gt",
BIGGER_EQ: "$gte",
SMALLER: "$lt",
SMALLER_EQ: "$lte",
// BETWEEN = "BETWEEN",
NOT: "$not",
IN: "$in",
// IS = "IS",
REGEXP: "$regex",
};
/**
* @description Mapping of logical operator names to CouchDB Mango query operators
* @summary Constants for CouchDB logical operators used in Mango queries
* @typedef {Object} CouchDBGroupOperatorType
* @property {string} AND - Logical AND operator ($and)
* @property {string} OR - Logical OR operator ($or)
* @const CouchDBGroupOperator
* @type {CouchDBGroupOperatorType}
* @memberOf module:for-couchdb
*/
const CouchDBGroupOperator = {
AND: "$and",
OR: "$or",
};
/**
* @description Special constant values used in CouchDB queries
* @summary String constants representing special values in CouchDB
* @typedef {Object} CouchDBConstType
* @property {string} NULL - String representation of null value
* @const CouchDBConst
* @type {CouchDBConstType}
* @memberOf module:for-couchdb
*/
const CouchDBConst = {
NULL: "null",
};
/**
* @description Generates a name for a CouchDB index
* @summary Creates a standardized name for a CouchDB index by combining name parts, compositions, and direction
* @param {string[]} name - Array of name parts for the index
* @param {OrderDirection} [direction] - Optional sort direction for the index
* @param {string[]} [compositions] - Optional additional attributes to include in the index name
* @param {string} [separator=DefaultSeparator] - The separator to use between parts of the index name
* @return {string} The generated index name
* @memberOf module:for-couchdb
*/
function generateIndexName$1(name, direction, compositions, separator = DefaultSeparator) {
return [
...name.map((n) => (n === CouchDBKeys.TABLE ? "table" : n)),
...([]),
...([]),
CouchDBKeys.INDEX,
].join(separator);
}
/**
* @description Generates CouchDB index configurations for models
* @summary Creates a set of CouchDB index configurations based on the metadata of the provided models
* @template M - The model type that extends Model
* @param models - Array of model constructors to generate indexes for
* @return {CreateIndexRequest[]} Array of CouchDB index configurations
* @function generateIndexes
* @memberOf module:for-couchdb
* @mermaid
* sequenceDiagram
* participant Caller
* participant generateIndexes
* participant generateIndexName
* participant Repository
*
* Caller->>generateIndexes: models
*
* Note over generateIndexes: Create base table index
* generateIndexes->>generateIndexName: [CouchDBKeys.TABLE]
* generateIndexName-->>generateIndexes: tableName
* generateIndexes->>generateIndexes: Create table index config
*
* loop For each model
* generateIndexes->>Repository: Get indexes metadata
* Repository-->>generateIndexes: index metadata
*
* loop For each index in metadata
* Note over generateIndexes: Extract index properties
* generateIndexes->>Repository: Get table name
* Repository-->>generateIndexes: tableName
*
* Note over generateIndexes: Define nested generate function
*
* generateIndexes->>generateIndexes: Call generate() for default order
* Note over generateIndexes: Create index name and config
*
* alt Has directions
* loop For each direction
* generateIndexes->>generateIndexes: Call generate(direction)
* Note over generateIndexes: Create ordered index config
* end
* end
* end
* end
*
* generateIndexes-->>Caller: Array of index configurations
*/
function generateIndexes(models) {
const tableName = generateIndexName$1([CouchDBKeys.TABLE]);
const indexes = {};
indexes[tableName] = {
index: {
fields: [CouchDBKeys.TABLE],
},
name: tableName,
ddoc: tableName,
type: "json",
};
models.forEach((m) => {
const ind = Repository.indexes(m);
Object.entries(ind).forEach(([key, value]) => {
const k = Object.keys(value)[0];
// eslint-disable-next-line prefer-const
let { directions, compositions } = value[k];
const tableName = Repository.table(m);
compositions = compositions || [];
function generate(sort) {
const name = [
tableName,
key,
...compositions,
PersistenceKeys.INDEX,
].join(DefaultSeparator);
indexes[name] = {
index: {
fields: [key, ...compositions, CouchDBKeys.TABLE].reduce((accum, el) => {
if (sort) {
const res = {};
res[el] = sort;
accum.push(res);
}
else {
accum.push(el);
}
return accum;
}, []),
},
name: name,
ddoc: name,
type: "json",
};
if (!sort) {
const tableFilter = {};
tableFilter[CouchDBKeys.TABLE] = {};
tableFilter[CouchDBKeys.TABLE][CouchDBOperator.EQUAL] = tableName;
indexes[name].index.partial_filter_selector = tableFilter;
}
}
generate();
if (directions)
directions.forEach((d) => generate(d));
});
});
return Object.values(indexes);
}
/**
* @description Model for CouchDB sequence records
* @summary Represents a sequence in CouchDB used for generating sequential IDs
* @param {ModelArg<Sequence>} [seq] - Optional initialization data for the sequence
* @class
* @example
* // Example of creating and using a Sequence
* const sequence = new Sequence({ id: 'user-seq', current: 1 });
* // Increment the sequence
* sequence.current = Number(sequence.current) + 1;
*/
let Sequence = class Sequence extends BaseModel {
constructor(seq) {
super(seq);
}
};
__decorate([
pk(),
__metadata("design:type", String)
], Sequence.prototype, "id", void 0);
__decorate([
required(),
index(),
__metadata("design:type", Object)
], Sequence.prototype, "current", void 0);
Sequence = __decorate([
table(CouchDBKeys.SEQUENCE),
model(),
__metadata("design:paramtypes", [Object])
], Sequence);
/**
* @summary Abstract implementation of a Sequence
* @description provides the basic functionality for {@link Sequence}s
*
* @param {SequenceOptions} options
*
* @class CouchDBSequence
* @implements Sequence
*/
class CouchDBSequence extends Sequence$1 {
constructor(options, adapter) {
super(options);
this.repo = Repository.forModel(Sequence, adapter.alias);
}
/**
* @summary Retrieves the current value for the sequence
* @protected
*/
async current() {
const { name, startWith } = this.options;
try {
const sequence = await this.repo.read(name);
return this.parse(sequence.current);
}
catch (e) {
if (e instanceof NotFoundError) {
if (typeof startWith === "undefined")
throw new InternalError("Starting value is not defined for a non existing sequence");
try {
return this.parse(startWith);
}
catch (e) {
throw new InternalError(`Failed to parse initial value for sequence ${startWith}: ${e}`);
}
}
throw new InternalError(`Failed to retrieve current value for sequence ${name}: ${e}`);
}
}
/**
* @summary Parses the {@link Sequence} value
*
* @protected
* @param value
*/
parse(value) {
return Sequence$1.parseValue(this.options.type, value);
}
/**
* @summary increments the sequence
* @description Sequence specific implementation
*
* @param {string | number | bigint} current
* @param count
* @protected
*/
async increment(current, count) {
const { type, incrementBy, name } = this.options;
let next;
const toIncrementBy = count || incrementBy;
if (toIncrementBy % incrementBy !== 0)
throw new InternalError(`Value to increment does not consider the incrementBy setting: ${incrementBy}`);
switch (type) {
case "Number":
next = this.parse(current) + toIncrementBy;
break;
case "BigInt":
next = this.parse(current) + BigInt(toIncrementBy);
break;
default:
throw new InternalError("Should never happen");
}
let seq;
try {
seq = await this.repo.update(new Sequence({ id: name, current: next }));
}
catch (e) {
if (!(e instanceof NotFoundError))
throw e;
seq = await this.repo.create(new Sequence({ id: name, current: next }));
}
return seq.current;
}
/**
* @summary Generates the next value in th sequence
* @description calls {@link Sequence#parse} on the current value
* followed by {@link Sequence#increment}
*
*/
async next() {
const current = await this.current();
return this.increment(current);
}
async range(count) {
const current = (await this.current());
const incrementBy = this.parse(this.options.incrementBy);
const next = await this.increment(current, this.parse(count) * incrementBy);
const range = [];
for (let i = 1; i <= count; i++) {
range.push(current + incrementBy * this.parse(i));
}
if (range[range.length - 1] !== next)
throw new InternalError("Miscalculation of range");
return range;
}
}
/**
* @description Error thrown when there is an issue with CouchDB indexes
* @summary Represents an error related to CouchDB index operations
* @param {string|Error} msg - The error message or Error object
* @class
* @category Errors
* @example
* // Example of using IndexError
* try {
* // Some code that might throw an index error
* throw new IndexError("Index not found");
* } catch (error) {
* if (error instanceof IndexError) {
* console.error("Index error occurred:", error.message);
* }
* }
*/
class IndexError extends BaseError {
constructor(msg) {
super(IndexError.name, msg, 404);
}
}
/**
* @description Paginator for CouchDB query results
* @summary Implements pagination for CouchDB queries using bookmarks for efficient navigation through result sets
* @template M - The model type that extends Model
* @template R - The result type
* @param {CouchDBAdapter<any, any, any>} adapter - The CouchDB adapter
* @param {MangoQuery} query - The Mango query to paginate
* @param {number} size - The page size
* @param {Constructor<M>} clazz - The model constructor
* @class CouchDBPaginator
* @example
* // Example of using CouchDBPaginator
* const adapter = new MyCouchDBAdapter(scope);
* const query = { selector: { type: "user" } };
* const paginator = new CouchDBPaginator(adapter, query, 10, User);
*
* // Get the first page
* const page1 = await paginator.page(1);
*
* // Get the next page
* const page2 = await paginator.page(2);
*/
class CouchDBPaginator extends Paginator {
/**
* @description Gets the total number of pages
* @summary Not supported in CouchDB - throws an error when accessed
* @return {number} Never returns as it throws an error
* @throws {InternalError} Always throws as this functionality is not available in CouchDB
*/
get total() {
throw new InternalError(`The total pages api is not available for couchdb`);
}
/**
* @description Gets the total record count
* @summary Not supported in CouchDB - throws an error when accessed
* @return {number} Never returns as it throws an error
* @throws {InternalError} Always throws as this functionality is not available in CouchDB
*/
get count() {
throw new InternalError(`The record count api is not available for couchdb`);
}
/**
* @description Creates a new CouchDBPaginator instance
* @summary Initializes a paginator for CouchDB query results
* @param {CouchDBAdapter<any, any, any>} adapter - The CouchDB adapter
* @param {MangoQuery} query - The Mango query to paginate
* @param {number} size - The page size
* @param {Constructor<M>} clazz - The model constructor
*/
constructor(adapter, query, size, clazz) {
super(adapter, query, size, clazz);
}
/**
* @description Prepares a query for pagination
* @summary Modifies the raw query to include pagination parameters
* @param {MangoQuery} rawStatement - The original Mango query
* @return {MangoQuery} The prepared query with pagination parameters
*/
prepare(rawStatement) {
const query = Object.assign({}, rawStatement);
if (query.limit)
this.limit = query.limit;
query.limit = this.size;
return query;
}
/**
* @description Retrieves a specific page of results
* @summary Executes the query with pagination and processes the results
* @param {number} [page=1] - The page number to retrieve
* @return {Promise<R[]>} A promise that resolves to an array of results
* @throws {PagingError} If trying to access a page other than the first without a bookmark, or if no class is defined
* @mermaid
* sequenceDiagram
* participant Client
* participant CouchDBPaginator
* participant Adapter
* participant CouchDB
*
* Client->>CouchDBPaginator: page(pageNumber)
* Note over CouchDBPaginator: Clone statement
* CouchDBPaginator->>CouchDBPaginator: validatePage(page)
*
* alt page !== 1
* CouchDBPaginator->>CouchDBPaginator: Check bookmark
* alt No bookmark
* CouchDBPaginator-->>Client: Throw PagingError
* else Has bookmark
* CouchDBPaginator->>CouchDBPaginator: Add bookmark to statement
* end
* end
*
* CouchDBPaginator->>Adapter: raw(statement, false)
* Adapter->>CouchDB: Execute query
* CouchDB-->>Adapter: Return results
* Adapter-->>CouchDBPaginator: Return MangoResponse
*
* Note over CouchDBPaginator: Process results
*
* alt Has warning
* CouchDBPaginator->>CouchDBPaginator: Log warning
* end
*
* CouchDBPaginator->>CouchDBPaginator: Check for clazz
*
* alt No clazz
* CouchDBPaginator-->>Client: Throw PagingError
* else Has clazz
* CouchDBPaginator->>CouchDBPaginator: Find primary key
*
* alt Has fields in statement
* CouchDBPaginator->>CouchDBPaginator: Use docs directly
* else No fields
* CouchDBPaginator->>CouchDBPaginator: Process each document
* loop For each document
* CouchDBPaginator->>CouchDBPaginator: Extract original ID
* CouchDBPaginator->>Adapter: revert(doc, clazz, pkDef.id, parsedId)
* end
* end
*
* CouchDBPaginator->>CouchDBPaginator: Store bookmark
* CouchDBPaginator->>CouchDBPaginator: Update currentPage
* CouchDBPaginator-->>Client: Return results
* end
*/
async page(page = 1) {
const statement = Object.assign({}, this.statement);
if (!this._recordCount || !this._totalPages) {
this._totalPages = this._recordCount = 0;
const results = await this.adapter.raw({ ...statement, limit: undefined }) || [];
this._recordCount = results.length;
if (this._recordCount > 0) {
const size = statement?.limit || this.size;
this._totalPages = Math.ceil(this._recordCount / size);
}
}
this.validatePage(page);
if (page !== 1) {
if (!this.bookMark)
throw new PagingError("No bookmark. Did you start in the first page?");
statement["bookmark"] = this.bookMark;
}
const rawResult = await this.adapter.raw(statement, false);
const { docs, bookmark, warning } = rawResult;
if (warning)
console.warn(warning);
if (!this.clazz)
throw new PagingError("No statement target defined");
const pkDef = findPrimaryKey(new this.clazz());
const results = statement.fields && statement.fields.length
? docs // has fields means its not full model
: docs.map((d) => {
//no fields means we need to revert to saving process
const originalId = d._id.split(CouchDBKeys.SEPARATOR);
originalId.splice(0, 1); // remove the table name
return this.adapter.revert(d, this.clazz, pkDef.id, Sequence$1.parseValue(pkDef.props.type, originalId.join(CouchDBKeys.SEPARATOR)));
});
this.bookMark = bookmark;
this._currentPage = page;
return results;
}
}
/**
* @description Translates core operators to CouchDB Mango operators
* @summary Converts Decaf.ts core operators to their equivalent CouchDB Mango query operators
* @param {GroupOperator | Operator} operator - The core operator to translate
* @return {MangoOperator} The equivalent CouchDB Mango operator
* @throws {QueryError} If no translation exists for the given operator
* @function translateOperators
* @memberOf module:for-couchdb
* @mermaid
* sequenceDiagram
* participant Caller
* participant translateOperators
* participant CouchDBOperator
* participant CouchDBGroupOperator
*
* Caller->>translateOperators: operator
*
* translateOperators->>CouchDBOperator: Check for match
* alt Found in CouchDBOperator
* CouchDBOperator-->>translateOperators: Return matching operator
* translateOperators-->>Caller: Return MangoOperator
* else Not found
* translateOperators->>CouchDBGroupOperator: Check for match
* alt Found in CouchDBGroupOperator
* CouchDBGroupOperator-->>translateOperators: Return matching operator
* translateOperators-->>Caller: Return MangoOperator
* else Not found
* translateOperators-->>Caller: Throw QueryError
* end
* end
*/
function translateOperators(operator) {
for (const operators of [CouchDBOperator, CouchDBGroupOperator]) {
const el = Object.keys(operators).find((k) => k === operator);
if (el)
return operators[el];
}
throw new QueryError(`Could not find adapter translation for operator ${operator}`);
}
/**
* @description Statement builder for CouchDB Mango queries
* @summary Provides a fluent interface for building CouchDB Mango queries with type safety
* @template M - The model type that extends Model
* @template R - The result type
* @param adapter - The CouchDB adapter
* @class CouchDBStatement
* @example
* // Example of using CouchDBStatement
* const adapter = new MyCouchDBAdapter(scope);
* const statement = new CouchDBStatement<User, User[]>(adapter);
*
* // Build a query
* const users = await statement
* .from(User)
* .where(Condition.attribute<User>('age').gt(18))
* .orderBy('lastName', 'asc')
* .limit(10)
* .execute();
*/
class CouchDBStatement extends Statement {
constructor(adapter) {
super(adapter);
}
/**
* @description Builds a CouchDB Mango query from the statement
* @summary Converts the statement's conditions, selectors, and options into a CouchDB Mango query
* @return {MangoQuery} The built Mango query
* @throws {Error} If there are invalid query conditions
* @mermaid
* sequenceDiagram
* participant Statement
* participant Repository
* participant parseCondition
*
* Statement->>Statement: build()
* Note over Statement: Initialize selectors
* Statement->>Repository: Get table name
* Repository-->>Statement: Return table name
* Statement->>Statement: Create base query
*
* alt Has selectSelector
* Statement->>Statement: Add fields to query
* end
*
* alt Has whereCondition
* Statement->>Statement: Create combined condition with table
* Statement->>parseCondition: Parse condition
* parseCondition-->>Statement: Return parsed condition
*
* alt Is group operator
* alt Is AND operator
* Statement->>Statement: Flatten nested AND conditions
* else Is OR operator
* Statement->>Statement: Combine with table condition
* else
* Statement->>Statement: Throw error
* end
* else
* Statement->>Statement: Merge conditions with existing selector
* end
* end
*
* alt Has orderBySelector
* Statement->>Statement: Add sort to query
* Statement->>Statement: Ensure field exists in selector
* end
*
* alt Has limitSelector
* Statement->>Statement: Set limit
* else
* Statement->>Statement: Use default limit
* end
*
* alt Has offsetSelector
* Statement->>Statement: Set skip
* end
*
* Statement-->>Statement: Return query
*/
build() {
const selectors = {};
selectors[CouchDBKeys.TABLE] = {};
selectors[CouchDBKeys.TABLE] = Repository.table(this.fromSelector);
const query = { selector: selectors };
if (this.selectSelector)
query.fields = this.selectSelector;
if (this.whereCondition) {
const condition = this.parseCondition(Condition.and(this.whereCondition, Condition.attribute(CouchDBKeys.TABLE).eq(query.selector[CouchDBKeys.TABLE]))).selector;
const selectorKeys = Object.keys(condition);
if (selectorKeys.length === 1 &&
Object.values(CouchDBGroupOperator).indexOf(selectorKeys[0]) !== -1)
switch (selectorKeys[0]) {
case CouchDBGroupOperator.AND:
condition[CouchDBGroupOperator.AND] = [
...Object.values(condition[CouchDBGroupOperator.AND]).reduce((accum, val) => {
const keys = Object.keys(val);
if (keys.length !== 1)
throw new Error("Too many keys in query selector. should be one");
const k = keys[0];
if (k === CouchDBGroupOperator.AND)
accum.push(...val[k]);
else
accum.push(val);
return accum;
}, []),
];
query.selector = condition;
break;
case CouchDBGroupOperator.OR: {
const s = {};
s[CouchDBGroupOperator.AND] = [
condition,
...Object.entries(query.selector).map(([key, val]) => {
const result = {};
result[key] = val;
return result;
}),
];
query.selector = s;
break;
}
default:
throw new Error("This should be impossible");
}
else {
Object.entries(condition).forEach(([key, val]) => {
if (query.selector[key])
console.warn(`A ${key} query param is about to be overridden: ${query.selector[key]} by ${val}`);
query.selector[key] = val;
});
}
}
if (this.orderBySelector) {
query.sort = query.sort || [];
query.selector = query.selector || {};
const [selector, value] = this.orderBySelector;
const rec = {};
rec[selector] = value;
query.sort.push(rec);
if (!query.selector[selector]) {
query.selector[selector] = {};
query.selector[selector][CouchDBOperator.BIGGER] =
null;
}
}
if (this.limitSelector) {
query.limit = this.limitSelector;
}
else {
console.warn(`No limit selector defined. Using default couchdb limit of ${CouchDBQueryLimit}`);
query.limit = CouchDBQueryLimit;
}
if (this.offsetSelector)
query.skip = this.offsetSelector;
return query;
}
/**
* @description Creates a paginator for the statement
* @summary Builds the query and returns a CouchDBPaginator for paginated results
* @template R - The result type
* @param {number} size - The page size
* @return {Promise<Paginator<M, R, MangoQuery>>} A promise that resolves to a paginator
* @throws {InternalError} If there's an error building the query
*/
async paginate(size) {
try {
const query = this.build();
return new CouchDBPaginator(this.adapter, query, size, this.fromSelector);
}
catch (e) {
throw new InternalError(e);
}
}
/**
* @description Processes a record from CouchDB
* @summary Extracts the ID from a CouchDB document and reverts it to a model instance
* @param {any} r - The raw record from CouchDB
* @param pkAttr - The primary key attribute of the model
* @param {"Number" | "BigInt" | undefined} sequenceType - The type of the sequence
* @return {any} The processed record
*/
processRecord(r, pkAttr, sequenceType) {
if (r[CouchDBKeys.ID]) {
const [, ...keyArgs] = r[CouchDBKeys.ID].split(CouchDBKeys.SEPARATOR);
const id = keyArgs.join("_");
return this.adapter.revert(r, this.fromSelector, pkAttr, Sequence$1.parseValue(sequenceType, id));
}
return r;
}
/**
* @description Executes a raw Mango query
* @summary Sends a raw Mango query to CouchDB and processes the results
* @template R - The result type
* @param {MangoQuery} rawInput - The raw Mango query to execute
* @return {Promise<R>} A promise that resolves to the query results
*/
async raw(rawInput) {
const results = await this.adapter.raw(rawInput, true);
const pkDef = findPrimaryKey(new this.fromSelector());
const pkAttr = pkDef.id;
const type = pkDef.props.type;
if (!this.selectSelector)
return results.map((r) => this.processRecord(r, pkAttr, type));
return results;
}
/**
* @description Parses a condition into a CouchDB Mango query selector
* @summary Converts a Condition object into a CouchDB Mango query selector structure
* @param {Condition<M>} condition - The condition to parse
* @return {MangoQuery} The Mango query with the parsed condition as its selector
* @mermaid
* sequenceDiagram
* participant Statement
* participant translateOperators
* participant merge
*
* Statement->>Statement: parseCondition(condition)
*
* Note over Statement: Extract condition parts
*
* alt Simple comparison operator
* Statement->>translateOperators: translateOperators(operator)
* translateOperators-->>Statement: Return CouchDB operator
* Statement->>Statement: Create selector with attribute and operator
* else NOT operator
* Statement->>Statement: parseCondition(attr1)
* Statement->>translateOperators: translateOperators(Operator.NOT)
* translateOperators-->>Statement: Return CouchDB NOT operator
* Statement->>Statement: Create negated selector
* else AND/OR operator
* Statement->>Statement: parseCondition(attr1)
* Statement->>Statement: parseCondition(comparison)
* Statement->>translateOperators: translateOperators(operator)
* translateOperators-->>Statement: Return CouchDB group operator
* Statement->>merge: merge(operator, op1, op2)
* merge-->>Statement: Return merged selector
* end
*
* Statement-->>Statement: Return query with selector
*/
parseCondition(condition) {
/**
* @description Merges two selectors with a logical operator
* @summary Helper function to combine two selectors with a logical operator
* @param {MangoOperator} op - The operator to use for merging
* @param {MangoSelector} obj1 - The first selector
* @param {MangoSelector} obj2 - The second selector
* @return {MangoQuery} The merged query
*/
function merge(op, obj1, obj2) {
const result = { selector: {} };
result.selector[op] = [obj1, obj2];
return result;
}
const { attr1, operator, comparison } = condition;
let op = {};
if ([GroupOperator.AND, GroupOperator.OR, Operator.NOT].indexOf(operator) === -1) {
op[attr1] = {};
op[attr1][translateOperators(operator)] =
comparison;
}
else if (operator === Operator.NOT) {
op = this.parseCondition(attr1).selector;
op[translateOperators(Operator.NOT)] = {};
op[translateOperators(Operator.NOT)][attr1.attr1] = comparison;
}
else {
const op1 = this.parseCondition(attr1).selector;
const op2 = this.parseCondition(comparison).selector;
op = merge(translateOperators(operator), op1, op2).selector;
}
return { selector: op };
}
}
/**
* @description Abstract adapter for CouchDB database operations
* @summary Provides a base implementation for CouchDB database operations, including CRUD operations, sequence management, and error handling
* @template Y - The scope type
* @template F - The repository flags type
* @template C - The context type
* @param {Y} scope - The scope for the adapter
* @param {string} flavour - The flavour of the adapter
* @param {string} [alias] - Optional alias for the adapter
* @class
* @example
* // Example of extending CouchDBAdapter
* class MyCouchDBAdapter extends CouchDBAdapter<MyScope, MyFlags, MyContext> {
* constructor(scope: MyScope) {
* super(scope, 'my-couchdb', 'my-alias');
* }
*
* // Implement abstract methods
* async index<M extends Model>(...models: Constructor<M>[]): Promise<void> {
* // Implementation
* }
*
* async raw<R>(rawInput: MangoQuery, docsOnly: boolean): Promise<R> {
* // Implementation
* }
*
* async create(tableName: string, id: string | number, model: Record<string, any>, ...args: any[]): Promise<Record<string, any>> {
* // Implementation
* }
*
* async read(tableName: string, id: string | number, ...args: any[]): Promise<Record<string, any>> {
* // Implementation
* }
*
* async update(tableName: string, id: string | number, model: Record<string, any>, ...args: any[]): Promise<Record<string, any>> {
* // Implementation
* }
*
* async delete(tableName: string, id: string | number, ...args: any[]): Promise<Record<string, any>> {
* // Implementation
* }
* }
*/
class CouchDBAdapter extends Adapter {
constructor(scope, flavour, alias) {
super(scope, flavour, alias);
[this.create, this.createAll, this.update, this.updateAll].forEach((m) => {
const name = m.name;
prefixMethod(this, m, this[name + "Prefix"]);
});
}
/**
* @description Creates a new CouchDB statement for querying
* @summary Factory method that creates a new CouchDBStatement instance for building queries
* @template M - The model type
* @return {CouchDBStatement<M, any>} A new CouchDBStatement instance
*/
Statement() {
return new CouchDBStatement(this);
}
/**
* @description Creates a new CouchDB sequence
* @summary Factory method that creates a new CouchDBSequence instance for managing sequences
* @param {SequenceOptions} options - The options for the sequence
* @return {Promise<Sequence>} A promise that resolves to a new Sequence instance
*/
async Sequence(options) {
return new CouchDBSequence(options, this);
}
/**
* @description Initializes the adapter by creating indexes for all managed models
* @summary Sets up the necessary database indexes for all models managed by this adapter
* @return {Promise<void>} A promise that resolves when initialization is complete
*/
async initialize() {
const managedModels = Adapter.models(this.flavour);
return this.index(...managedModels);
}
/**
* @description Assigns metadata to a model
* @summary Adds revision metadata to a model as a non-enumerable property
* @param {Record<string, any>} model - The model to assign metadata to
* @param {string} rev - The revision string to assign
* @return {Record<string, any>} The model with metadata assigned
*/
assignMetadata(model, rev) {
Object.defineProperty(model, PersistenceKeys.METADATA, {
enumerable: false,
configurable: false,
writable: false,
value: rev,
});
return model;
}
/**
* @description Assigns metadata to multiple models
* @summary Adds revision metadata to multiple models as non-enumerable properties
* @param models - The models to assign metadata to
* @param {string[]} revs - The revision strings to assign
* @return The models with metadata assigned
*/
assignMultipleMetadata(models, revs) {
models.forEach((m, i) => {
Repository.setMetadata(m, revs[i]);
return m;
});
return models;
}
/**
* @description Prepares a record for creation
* @summary Adds necessary CouchDB fields to a record before creation
* @param {string} tableName - The name of the table
* @param {string|number} id - The ID of the record
* @param {Record<string, any>} model - The model to prepare
* @return A tuple containing the tableName, id, and prepared record
*/
createPrefix(tableName, id, model) {
const record = {};
record[CouchDBKeys.TABLE] = tableName;
record[CouchDBKeys.ID] = this.generateId(tableName, id);
Object.assign(record, model);
return [tableName, id, record];
}
/**
* @description Prepares multiple records for creation
* @summary Adds necessary CouchDB fields to multiple records before creation
* @param {string} tableName - The name of the table
* @param {string[]|number[]} ids - The IDs of the records
* @param models - The models to prepare
* @return A tuple containing the tableName, ids, and prepared records
* @throws {InternalError} If ids and models arrays have different lengths
*/
createAllPrefix(tableName, ids, models) {
if (ids.length !== models.length)
throw new InternalError("Ids and models must have the same length");
const records = ids.map((id, count) => {
const record = {};
record[CouchDBKeys.TABLE] = tableName;
record[CouchDBKeys.ID] = this.generateId(tableName, id);
Object.assign(record, models[count]);
return record;
});
return [tableName, ids, records];
}
/**
* @description Prepares a record for update
* @summary Adds necessary CouchDB fields to a record before update
* @param {string} tableName - The name of the table
* @param {string|number} id - The ID of the record
* @param model - The model to prepare
* @return A tuple containing the tableName, id, and prepared record
* @throws {InternalError} If no revision number is found in the model
*/
updatePrefix(tableName, id, model) {
const record = {};
record[CouchDBKeys.TABLE] = tableName;
record[CouchDBKeys.ID] = this.generateId(tableName, id);
const rev = model[PersistenceKeys.METADATA];
if (!rev)
throw new InternalError(`No revision number found for record with id ${id}`);
Object.assign(record, model);
record[CouchDBKeys.REV] = rev;
return [tableName, id, record];
}
/**
* @description Prepares multiple records for update
* @summary Adds necessary CouchDB fields to multiple records before update
* @param {string} tableName - The name of the table
* @param {string[]|number[]} ids - The IDs of the records
* @param models - The models to prepare
* @return A tuple containing the tableName, ids, and prepared records
* @throws {InternalError} If ids and models arrays have different lengths or if no revision number is found in a model
*/
updateAllPrefix(tableName, ids, models) {
if (ids.length !== models.length)
throw new InternalError("Ids and models must have the same length");
const records = ids.map((id, count) => {
const record = {};
record[CouchDBKeys.TABLE] = tableName;
record[CouchDBKeys.ID] = this.generateId(tableName, id);
const rev = models[count][PersistenceKeys.METADATA];
if (!rev)
throw new InternalError(`No revision number found for record with id ${id}`);
Object.assign(record, models[count]);
record[CouchDBKeys.REV] = rev;
return record;
});
return [tableName, ids, records];
}
/**
* @description Generates a CouchDB document ID
* @summary Combines the table name and ID to create a CouchDB document ID
* @param {string} tableName - The name of the table
* @param {string|number} id - The ID of the record
* @return {string} The generated CouchDB document ID
*/
generateId(tableName, id) {
return [tableName, id].join(CouchDBKeys.SEPARATOR);
}
/**
* @description Parses an error and converts it to a BaseError
* @summary Converts various error types to appropriate BaseError subtypes
* @param {Error|string} err - The error to parse
* @param {string} [reason] - Optional reason for the error
* @return {BaseError} The parsed error as a BaseError
*/
parseError(err, reason) {
return CouchDBAdapter.parseError(err, reason);
}
/**
* @description Checks if an attribute is reserved
* @summary Determines if an attribute name is reserved in CouchDB
* @param {string} attr - The attribute name to check
* @return {boolean} True if the attribute is reserved, false otherwise
*/
isReserved(attr) {
return !!attr.match(reservedAttributes);
}
/**
* @description Static method to parse an error and convert it to a BaseError
* @summary Converts various error types to appropriate BaseError subtypes based on error codes and messages
* @param {Error|string} err - The error to parse
* @param {string} [reason] - Optional reason for the error
* @return {BaseError} The parsed error as a BaseError
* @mermaid
* sequenceDiagram
* participant Caller
* participant parseError
* participant ErrorTypes
*
* Caller->>parseError: err, reason
* Note over parseError: Check if err is already a BaseError
* alt err is BaseError
* parseError-->>Caller: return err
* else err is string
* Note over parseError: Extract code from string
* alt code matches "already exist|update conflict"
* parseError->>ErrorTypes: new ConflictError(code)
* ErrorTypes-->>Caller: ConflictError
* else code matches "missing|deleted"
* parseError->>ErrorTypes: new NotFoundError(code)
* ErrorTypes-->>Caller: NotFoundError
* end
* else err has code property
* Note over parseError: Extract code and reason
* else err has statusCode property
* Note over parseError: Extract code and reason
* else
* Note over parseError: Use err.message as code
* end
*
* Note over parseError: Switch on code
* alt code is 401, 412, or 409
* parseError->>ErrorTypes: new ConflictError(reason)
* ErrorTypes-->>Caller: ConflictError
* else code is 404
* parseError->>ErrorTypes: new NotFoundError(reason)
* ErrorTypes-->>Caller: NotFoundError
* else code is 400
* alt code matches "No index exists"
* parseError->>ErrorTypes: new IndexError(err)
* ErrorTypes-->>Caller: IndexError
* else
* parseError->>ErrorTypes: new InternalError(err)
* ErrorTypes-->>Caller: InternalError
* end
* else code matches "ECONNREFUSED"
* parseError->>ErrorTypes: new ConnectionError(err)
* ErrorTypes-->>Caller: ConnectionError
* else
* parseError->>ErrorTypes: new InternalError(err)
* ErrorTypes-->>Caller: InternalError
* end
*/
static parseError(err, reason) {
if (err instanceof BaseError)
return err;
let code = "";
if (typeof err === "string") {
code = err;
if (code.match(/already exist|update conflict/g))
return new ConflictError(code);
if (code.match(/missing|deleted/g))
return new NotFoundError(code);
}
else if (err.code) {
code = err.code;
reason = reason || err.message;
}
else if (err.statusCode) {
code = err.statusCode;
reason = reason || err.message;
}
else {
code = err.message;
}
switch (code.toString()) {
case "401":
case "412":
case "409":
return new ConflictError(reason);
case "404":
return new NotFoundError(reason);
case "400":
if (code.toString().match(/No\sindex\sexists/g))
return new IndexError(err);
return new InternalError(err);
default:
if (code.toString().match(/ECONNREFUSED/g))
return new ConnectionError(err);
return new InternalError(err);
}
}
}
__decorate([
final(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", CouchDBStatement)
], CouchDBAdapter.prototype, "Statement", null);
__decorate([
final(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], CouchDBAdapter.prototype, "Sequence", null);
__decorate([
final(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, String]),
__metadata("design:returntype", Object)
], CouchDBAdapter.prototype, "assignMetadata", null);
__decorate([
final(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Array, Array]),
__metadata("design:returntype", Array)
], CouchDBAdapter.prototype, "assignMultipleMetadata", null);
__decorate([
final(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, Object, Object]),
__metadata("design:returntype", void 0)
], CouchDBAdapter.prototype, "createPrefix", null);
__decorate([
final(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, Array, Array]),
__metadata("design:returntype", void 0)
], CouchDBAdapter.prototype, "createAllPrefix", null);
__decorate([
final(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, Object, Object]),
__metadata("design:returntype", void 0)
], CouchDBAdapter.prototype, "updatePrefix", null);
__decorate([
final(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, Array, Array]),
__metadata("design:returntype", void 0)
], CouchDBAdapter.prototype, "updateAllPrefix", null);
/**
* @description Re-authenticates a connection to CouchDB
* @summary Refreshes the authentication for a CouchDB connection using the provided credentials
* @param {any} con - The CouchDB connection object
* @param {string} user - The username for authentication
* @param {string} pass - The password for authentication
* @return {Promise<any>} A promise that resolves to the authentication result
* @function reAuth
* @memberOf module:for-couchdb
*/
async function reAuth(con, user, pass) {
return con.auth(user, pass);
}
/**
* @description Wraps a CouchDB database connection with automatic re-authentication
* @summary Creates a proxy around a CouchDB database connection that automatically re-authenticates before each operation
* @param {any} con - The CouchDB connection object
* @param {string} dbName - The name of the database to use
* @param {string} user - The username for authentication
* @param {string} pass - The password for authentication
* @return {any} The wrapped database connection object
* @function wrapDocumentScope
* @memberOf module:for-couchdb
* @mermaid
* sequenceDiagram
* participant Client
* participant wrapDocumentScope
* participant DB
* participant reAuth
*
* Client->>wrapDocumentScope: con, dbName, user, pass
* wrapDocumentScope->>DB: con.use(dbName)
* Note over wrapDocumentScope: Wrap DB methods with re-auth
*
* loop For each method (insert, get, put, destroy, find)
* wrapDocumentScope->>wrapDocumentScope: Store original method
* wrapDocumentScope->>wrapDocumentScope: Define new method with re-auth
* end
*
* wrapDocumentScope->>wrapDocumentScope: Add NATIVE property with con value
* wrapDocumentScope-->>Client: Retur