UNPKG

@decaf-ts/for-couchdb

Version:
1,277 lines (1,266 loc) 172 kB
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