UNPKG

@decaf-ts/for-couchdb

Version:
868 lines 37 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CouchDBStatement = void 0; const core_1 = require("@decaf-ts/core"); const decorator_validation_1 = require("@decaf-ts/decorator-validation"); const translate_js_1 = require("./translate.cjs"); const constants_js_1 = require("./../constants.cjs"); const constants_js_2 = require("./constants.cjs"); const db_decorators_1 = require("@decaf-ts/db-decorators"); const decoration_1 = require("@decaf-ts/decoration"); const generator_js_1 = require("./../views/generator.cjs"); const planner_js_1 = require("./planner.cjs"); const generator_js_2 = require("./../indexes/generator.cjs"); const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); function nextLexicographicString(value) { if (!value) return "\u0000"; const chars = Array.from(value); for (let i = chars.length - 1; i >= 0; i -= 1) { const code = chars[i].codePointAt(0); if (code === undefined) continue; if (code < 0x10ffff) { chars[i] = String.fromCodePoint(code + 1); return chars.slice(0, i + 1).join(""); } } return `${value}\u0000`; } function prefixRange(prefix) { return { start: prefix, end: nextLexicographicString(prefix), }; } /** * @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 core_1.Statement { constructor(adapter, overrides) { super(adapter, overrides); this.attributeTypeCache = new Map(); } /** * @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 */ toColumnName(attribute) { if (attribute === constants_js_1.CouchDBKeys.TABLE || attribute === constants_js_1.CouchDBKeys.SEQUENCE) return attribute; return decorator_validation_1.Model.columnName(this.fromSelector, attribute); } build() { const log = this.log.for(this.build); this.manualAggregation = undefined; const aggregateInfo = this.buildAggregateInfo(); if (aggregateInfo) { if (this.shouldUseManualAggregation()) { this.manualAggregation = aggregateInfo; } else { return this.createAggregateQuery(aggregateInfo); } } const selectors = {}; selectors[constants_js_1.CouchDBKeys.TABLE] = {}; selectors[constants_js_1.CouchDBKeys.TABLE] = decorator_validation_1.Model.tableName(this.fromSelector); const query = { selector: selectors }; if (this.selectSelector) query.fields = this.selectSelector.map((f) => this.toColumnName(f)); if (this.whereCondition) { const condition = this.parseCondition(core_1.Condition.and(this.whereCondition, core_1.Condition.attribute(constants_js_1.CouchDBKeys.TABLE).eq(query.selector[constants_js_1.CouchDBKeys.TABLE]))).selector; const selectorKeys = Object.keys(condition); if (selectorKeys.length === 1 && Object.values(constants_js_2.CouchDBGroupOperator).indexOf(selectorKeys[0]) !== -1) switch (selectorKeys[0]) { case constants_js_2.CouchDBGroupOperator.AND: condition[constants_js_2.CouchDBGroupOperator.AND] = [ ...Object.values(condition[constants_js_2.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 === constants_js_2.CouchDBGroupOperator.AND) accum.push(...val[k]); else accum.push(val); return accum; }, []), ]; query.selector = condition; break; case constants_js_2.CouchDBGroupOperator.OR: { const s = {}; s[constants_js_2.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]) log.warn(`A ${key} query param is about to be overridden: ${query.selector[key]} by ${val}`); query.selector[key] = val; }); } } if (this.orderBySelectors?.length) { query.sort = query.sort || []; query.selector = query.selector || {}; for (const [selectorKey, direction] of this.orderBySelectors) { const selector = this.toColumnName(selectorKey); const rec = {}; rec[selector] = direction; query.sort.push(rec); if (!query.selector[selector]) { query.selector[selector] = {}; query.selector[selector][constants_js_2.CouchDBOperator.BIGGER] = null; } } } const hasManualAggregate = !!this.manualAggregation; if (this.limitSelector) { query.limit = this.limitSelector; } else if (!hasManualAggregate) { log.warn(`No limit selector defined. Using default couchdb limit of ${constants_js_2.CouchDBQueryLimit}`); query.limit = constants_js_2.CouchDBQueryLimit; } if (this.offsetSelector) query.skip = this.offsetSelector; this.attachDefaultQueryIndex(query); const forceNamedIndexes = this.overrides ?.forceNamedIndexes ?? true; if (forceNamedIndexes) { (0, planner_js_1.attachGeneratedUseIndex)(this.fromSelector, query, log, { preserveDefaultQuery: true, requireSortCoverage: false, forceNamedIndexes: true, }); } (0, planner_js_1.warnScanProneMangoOperators)(query.selector, log); return query; } attachDefaultQueryIndex(query) { if (!this.fromSelector || query.use_index) return; const tableName = decorator_validation_1.Model.tableName(this.fromSelector); let defaultAttrs = []; try { defaultAttrs = decorator_validation_1.Model.defaultQueryAttributes(this.fromSelector); } catch { defaultAttrs = []; } if (!defaultAttrs.length) return; const rangedAttrs = this.collectStartsWithRangeAttrs(query.selector || {}); const matchedAttrs = defaultAttrs.filter((attr) => rangedAttrs.has(attr)); if (!matchedAttrs.length) return; const sort = Array.isArray(query.sort) ? query.sort : []; const sortEntry = sort .map((entry) => Object.entries(entry || {})[0]) .find((entry) => entry && matchedAttrs.includes(entry[0])); const targetAttr = sortEntry?.[0] || matchedAttrs[0]; const direction = sortEntry?.[1] || undefined; const indexName = (0, generator_js_2.generateIndexName)([tableName, targetAttr, "defaultQuery"], direction); query.use_index = indexName; } collectStartsWithRangeAttrs(selector) { const attrs = new Set(); const walk = (node) => { if (!node || typeof node !== "object") return; if (Array.isArray(node)) { node.forEach((child) => walk(child)); return; } Object.entries(node).forEach(([key, value]) => { if (key !== constants_js_2.CouchDBGroupOperator.AND && key !== constants_js_2.CouchDBGroupOperator.OR && value && typeof value === "object" && !Array.isArray(value)) { const hasLower = Object.prototype.hasOwnProperty.call(value, constants_js_2.CouchDBOperator.BIGGER_EQ); const hasUpper = Object.prototype.hasOwnProperty.call(value, constants_js_2.CouchDBOperator.SMALLER); if (hasLower && hasUpper) attrs.add(key); } walk(value); }); }; walk(selector); return attrs; } /** * @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(record, ctx) { const pkAttr = decorator_validation_1.Model.pk(this.fromSelector); const type = decoration_1.Metadata.get(this.fromSelector, decoration_1.Metadata.key(db_decorators_1.DBKeys.ID, pkAttr))?.type; if (record[constants_js_1.CouchDBKeys.ID]) { const [, ...keyArgs] = record[constants_js_1.CouchDBKeys.ID].split(constants_js_1.CouchDBKeys.SEPARATOR); const id = keyArgs.join("_"); record[pkAttr] = core_1.Sequence.parseValue(type, id); } return this.adapter.revert(record, this.fromSelector, record[pkAttr], undefined, ctx); } /** * @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, ...args) { const { ctx } = this.logCtx(args, this.raw); const aggregator = rawInput?.aggregateInfo; if (rawInput?.aggregate && aggregator) { return this.executeAggregate(aggregator, ctx); } // Scope the raw query to this statement's own table. build() does this for // the execute() path; raw() must do the same so a caller cannot use it to // dump or target documents belonging to other tables (cross-table leak). const scoped = this.scopeToTable(rawInput); const results = await this.adapter.raw(scoped, true, ctx); // Manual aggregation needs reverted docs to compute the aggregate. The // result is a computed value (not documents), so execute() returns it // unchanged via hasAggregation(). if (this.manualAggregation) { const processed = results.map((r) => this.processRecord(r, ctx)); const manualResult = this.executeManualAggregation(processed, this.manualAggregation, ctx); this.manualAggregation = undefined; return manualResult; } // groupBy (without selectSelector) needs reverted docs to build groups. // hasAggregation() is true for groupBy, so execute() returns this unchanged. if (!this.selectSelector && this.groupBySelectors?.length) { const processed = results.map((r) => this.processRecord(r, ctx)); return this.groupSelectResults(processed); } if (!this.selectSelector) { // Return raw docs and let execute() perform the single revert. The // previous implementation reverted here AND in execute(), causing a // double-revert that clobbered column-mapped fields (the second revert // looked up column names on already-reverted model instances). return results; } // selectSelector is set: process here (core Statement.raw() contract) and // return model instances. execute() returns these unchanged. const processor = (r) => this.processRecord(r, ctx); if (Array.isArray(results)) { const mapped = results.map(processor); return (await this.applyAfterHandlersToResult(mapped, ctx)); } const single = processor(results); return (await this.applyAfterHandlersToResult(single, ctx)); } /** * @description Forces a raw Mango query to be scoped to this statement's table * @summary Returns a copy of the input query whose selector includes the * ??table discriminator set to the statement's own table, so a caller cannot * read documents from other tables through raw(). Existing ??table selectors * are overridden (never trusted from the caller). * @param {MangoQuery} rawInput - The raw Mango query to scope * @return {MangoQuery} A new Mango query scoped to this statement's table */ scopeToTable(rawInput) { if (!this.fromSelector) return rawInput; const tableName = decorator_validation_1.Model.tableName(this.fromSelector); const base = rawInput && typeof rawInput === "object" ? rawInput : {}; const selector = Object.assign({}, base.selector || {}); // Force the discriminator; a caller-supplied value is never trusted. selector[constants_js_1.CouchDBKeys.TABLE] = tableName; return Object.assign({}, base, { selector }); } /** * @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 */ buildAggregateInfo() { if (!this.fromSelector) return undefined; if (this.avgSelector) { const attribute = String(this.avgSelector); const sumInfo = this.createAggregateDescriptor("sum", attribute); if (!sumInfo) throw this.missingDecorator("sum", attribute); const countInfo = this.createAggregateDescriptor("count", attribute); if (!countInfo) throw this.missingDecorator("count", attribute); return { kind: "avg", attribute, sumDescriptor: sumInfo.descriptor, countDescriptor: countInfo.descriptor, }; } if (typeof this.countDistinctSelector !== "undefined") { const attribute = this.resolveSelectorAttribute(this.countDistinctSelector); const info = this.createAggregateDescriptor("distinct", attribute); if (!info) throw this.missingDecorator("distinct", attribute); info.countDistinct = true; return info; } if (typeof this.countSelector !== "undefined") { const attribute = this.resolveSelectorAttribute(this.countSelector); const info = this.createAggregateDescriptor("count", attribute); if (!info) throw this.missingDecorator("count", attribute); return info; } if (this.maxSelector) { const attribute = this.resolveSelectorAttribute(this.maxSelector); const info = this.createAggregateDescriptor("max", attribute); if (!info) throw this.missingDecorator("max", attribute); return info; } if (this.minSelector) { const attribute = this.resolveSelectorAttribute(this.minSelector); const info = this.createAggregateDescriptor("min", attribute); if (!info) throw this.missingDecorator("min", attribute); return info; } if (this.sumSelector) { const attribute = this.resolveSelectorAttribute(this.sumSelector); const info = this.createAggregateDescriptor("sum", attribute); if (!info) throw this.missingDecorator("sum", attribute); return info; } if (this.distinctSelector) { const attribute = this.resolveSelectorAttribute(this.distinctSelector); const info = this.createAggregateDescriptor("distinct", attribute); if (!info) throw this.missingDecorator("distinct", attribute); return info; } return undefined; } createAggregateDescriptor(kind, attribute) { if (!this.fromSelector) return undefined; const metas = (0, generator_js_1.findViewMetadata)(this.fromSelector, kind, attribute); if (!metas.length) return undefined; const meta = metas[0]; const tableName = decorator_validation_1.Model.tableName(this.fromSelector); const viewName = (0, generator_js_1.generateViewName)(tableName, meta.attribute, kind, meta); const ddoc = meta.ddoc || (0, generator_js_1.generateDesignDocName)(tableName, viewName); const options = { reduce: meta.reduce !== undefined ? true : !meta.returnDocs, }; if (kind === "distinct" || kind === "groupBy") options.group = true; return { kind, meta, descriptor: { ddoc, view: viewName, options, }, }; } createAggregateQuery(info) { return { selector: {}, aggregate: true, aggregateInfo: info, }; } shouldUseManualAggregation() { return !!this.whereCondition; } async executeAggregate(info, ctx) { if (!this.isViewAggregate(info)) { return this.handleAverage(info, ctx); } const couchAdapter = this.getCouchAdapter(); const viewInfo = info; const response = await couchAdapter.view(viewInfo.descriptor.ddoc, viewInfo.descriptor.view, viewInfo.descriptor.options, ctx); return this.processViewResponse(info, response); } async handleAverage(info, ctx) { if (info.kind !== "avg") throw new core_1.QueryError("Average descriptor is not valid"); const [sumDesc, countDesc] = [info.sumDescriptor, info.countDescriptor]; const couchAdapter = this.getCouchAdapter(); const [sumResponse, countResponse] = await Promise.all([ couchAdapter.view(sumDesc.ddoc, sumDesc.view, sumDesc.options, ctx), couchAdapter.view(countDesc.ddoc, countDesc.view, countDesc.options, ctx), ]); const sum = sumResponse.rows?.[0]?.value ?? 0; const count = countResponse.rows?.[0]?.value ?? 0; if (!count) return 0; return (sum / count); } executeManualAggregation(docs, info, // eslint-disable-next-line @typescript-eslint/no-unused-vars ctx) { if (!this.fromSelector) throw new core_1.QueryError("Manual aggregation requires a target model"); if (info.kind === "avg") { return this.computeAverage(docs, info.attribute); } if (info.kind === "groupBy") { return this.computeGroupBy(docs, info.meta.attribute); } const attribute = info.meta.attribute; switch (info.kind) { case "count": { if (info.countDistinct) { return this.computeDistinctCount(docs, attribute); } return this.computeCount(docs, attribute); } case "distinct": if (info.countDistinct) { return this.computeDistinctCount(docs, attribute); } return this.computeDistinctValues(docs, attribute); case "sum": return this.computeSum(docs, attribute); case "min": return this.computeMinMax(docs, attribute, "min"); case "max": return this.computeMinMax(docs, attribute, "max"); default: throw new core_1.QueryError(`Unsupported manual aggregation ${info.kind}`); } } computeCount(docs, attribute) { if (!attribute) return docs.length; const values = this.collectValues(docs, attribute); return values.filter((value) => value !== undefined && value !== null) .length; } computeDistinctCount(docs, attribute) { const values = attribute ? this.collectValues(docs, attribute).filter((value) => value !== undefined && value !== null) : docs; const seen = new Set(); values.forEach((value) => seen.add(JSON.stringify(value))); return seen.size; } computeDistinctValues(docs, attribute) { if (!attribute) return []; const values = this.collectValues(docs, attribute); const seen = new Set(); const unique = []; values.forEach((value) => { const key = JSON.stringify(value); if (!seen.has(key)) { seen.add(key); unique.push(value); } }); return unique; } computeSum(docs, attribute) { if (!attribute) return docs.length; const values = this.collectValues(docs, attribute).filter((value) => value !== undefined && value !== null); const sum = values.reduce((acc, value) => acc + this.toNumericValue(value, attribute, "SUM operation"), 0); return sum; } computeAverage(docs, attribute) { if (!attribute) return 0; const values = this.collectValues(docs, attribute).filter((value) => value !== undefined && value !== null); if (!values.length) return 0; const sum = values.reduce((acc, value) => acc + this.toNumericValue(value, attribute, "AVG operation"), 0); return (sum / values.length); } computeMinMax(docs, attribute, mode) { if (!attribute) return null; const values = this.collectValues(docs, attribute).filter((value) => value !== undefined && value !== null); let currentValue = null; let currentComparable = null; for (const value of values) { const normalized = this.normalizeComparable(value); if (normalized === null) continue; if (currentComparable === null) { currentComparable = normalized; currentValue = value; continue; } if ((mode === "min" && normalized < currentComparable) || (mode === "max" && normalized > currentComparable)) { currentComparable = normalized; currentValue = value; } } return currentValue; } computeGroupBy(docs, attribute) { const grouped = {}; const values = this.collectValues(docs, attribute); docs.forEach((doc, index) => { const key = this.groupKey(values[index]); if (!grouped[key]) grouped[key] = []; grouped[key].push(doc); }); return grouped; } groupSelectResults(docs) { if (!this.groupBySelectors?.length) return {}; const attribute = this.resolveSelectorAttribute(this.groupBySelectors[0]); if (!attribute) return {}; const grouped = {}; docs.forEach((doc) => { const key = this.groupKey(this.convertValueByAttribute(attribute, doc[attribute])); if (!grouped[key]) grouped[key] = []; grouped[key].push(doc); }); return grouped; } collectValues(docs, attribute) { return docs.map((doc) => { if (!doc || typeof doc !== "object") return undefined; return this.convertValueByAttribute(attribute, doc[attribute]); }); } convertValueByAttribute(attribute, value) { if (!this.fromSelector) return value; const attributeType = this.getAttributeType(attribute); if (attributeType === "date") { if (value instanceof Date) return value; const parsed = new Date(value); if (!Number.isNaN(parsed.getTime())) return parsed; } if (typeof value === "string" && attribute.toLowerCase().includes("date")) { const parsed = new Date(value); if (!Number.isNaN(parsed.getTime())) return parsed; } return value; } getAttributeType(attribute) { if (!attribute || !this.fromSelector) return undefined; if (this.attributeTypeCache.has(attribute)) { return this.attributeTypeCache.get(attribute); } const metaType = decoration_1.Metadata.type(this.fromSelector, attribute) ?? decoration_1.Metadata.getPropDesignTypes?.(this.fromSelector, attribute) ?.designType; const normalized = this.normalizeMetaType(metaType); this.attributeTypeCache.set(attribute, normalized); return normalized; } normalizeMetaType(metaType) { if (!metaType) return undefined; if (typeof metaType === "string") return metaType.toLowerCase(); if (typeof metaType === "function" && metaType.name) return metaType.name.toLowerCase(); return undefined; } normalizeComparable(value) { if (typeof value === "number") return value; if (typeof value === "bigint") return Number(value); if (value instanceof Date) return value.getTime(); if (typeof value === "string" && !isNaN(Number(value))) return Number(value); return null; } groupKey(value) { if (value === undefined) return "undefined"; if (value === null) return "null"; if (typeof value === "symbol") return value.toString(); if (typeof value === "object") { try { return JSON.stringify(value); } catch { return String(value); } } return String(value); } toNumericValue(value, field, context) { if (typeof value === "number") return value; if (typeof value === "bigint") return Number(value); if (value instanceof Date) return value.getTime(); if (typeof value === "string" && !isNaN(Number(value))) return Number(value); throw new core_1.QueryError(`${context} on "${field}" requires numeric values, but got ${typeof value}`); } convertAggregateValue(attribute, value) { if (!attribute) return value; return this.convertValueByAttribute(attribute, value); } resolveSelectorAttribute(selector) { if (selector == null) return undefined; return String(selector); } missingDecorator(kind, attribute) { const decorator = this.decoratorForKind(kind); const table = this.fromSelector ? decorator_validation_1.Model.tableName(this.fromSelector) : "<unknown table>"; const attributeDesc = attribute ? ` on "${attribute}"` : ""; return new core_1.UnsupportedError(`${decorator} decorator is required for CouchDB ${kind} aggregation${attributeDesc} on table "${table}".`); } decoratorForKind(kind) { const map = { count: "@count", sum: "@sum", min: "@min", max: "@max", distinct: "@distinct", groupBy: "@groupBy", view: "@view", avg: "@avg", }; return map[kind] || `@${kind}`; } processViewResponse(info, response) { if (info.kind === "avg") throw new core_1.QueryError("Average results should be handled before processing rows"); const rows = response.rows || []; const viewInfo = info; const meta = viewInfo.meta; if (viewInfo.countDistinct) { return (rows.length || 0); } if (viewInfo.kind === "distinct" || viewInfo.kind === "groupBy") { return rows.map((row) => this.convertAggregateValue(viewInfo.meta.attribute, row.key ?? row.value)); } if (meta.returnDocs) { return rows.map((row) => row.value ?? row.doc ?? row); } if (!rows.length) { return (viewInfo.kind === "count" ? 0 : null); } return this.convertAggregateValue(viewInfo.meta.attribute, rows[0].value ?? rows[0].key ?? null); } isViewAggregate(info) { return info.kind !== "avg"; } getCouchAdapter() { return this.adapter; } 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; if (operator === core_1.Operator.STARTS_WITH) { if (typeof attr1 !== "string") throw new core_1.QueryError("STARTS_WITH requires an attribute name"); if (typeof comparison !== "string") throw new core_1.QueryError("STARTS_WITH requires a string comparison"); const range = prefixRange(comparison); return { selector: { [this.toColumnName(attr1)]: { [constants_js_2.CouchDBOperator.BIGGER_EQ]: range.start, [constants_js_2.CouchDBOperator.SMALLER]: range.end, }, }, }; } if (operator === core_1.Operator.ENDS_WITH) { if (typeof attr1 !== "string") throw new core_1.QueryError("ENDS_WITH requires an attribute name"); if (typeof comparison !== "string") throw new core_1.QueryError("ENDS_WITH requires a string comparison"); return { selector: { [this.toColumnName(attr1)]: { [constants_js_2.CouchDBOperator.REGEXP]: `${escapeRegExp(comparison)}$`, }, }, }; } if (operator === core_1.Operator.BETWEEN) { const attr = this.toColumnName(attr1); if (!Array.isArray(comparison) || comparison.length !== 2) throw new core_1.QueryError("BETWEEN operator requires [min, max] comparison"); const [min, max] = comparison; const opBetween = {}; opBetween[attr] = {}; opBetween[attr][(0, translate_js_1.translateOperators)(core_1.Operator.BIGGER_EQ)] = min; opBetween[attr][(0, translate_js_1.translateOperators)(core_1.Operator.SMALLER_EQ)] = max; return { selector: opBetween }; } let op = {}; if ([core_1.GroupOperator.AND, core_1.GroupOperator.OR, core_1.Operator.NOT].indexOf(operator) === -1) { const attr = this.toColumnName(attr1); op[attr] = {}; op[attr][(0, translate_js_1.translateOperators)(operator)] = comparison; } else if (operator === core_1.Operator.NOT) { op = this.parseCondition(attr1).selector; op[(0, translate_js_1.translateOperators)(core_1.Operator.NOT)] = {}; op[(0, translate_js_1.translateOperators)(core_1.Operator.NOT)][this.toColumnName(attr1.attr1)] = comparison; } else { const op1 = this.parseCondition(attr1).selector; const op2 = this.parseCondition(comparison).selector; op = merge((0, translate_js_1.translateOperators)(operator), op1, op2).selector; } return { selector: op }; } } exports.CouchDBStatement = CouchDBStatement; //# sourceMappingURL=Statement.js.map //# sourceMappingURL=Statement.cjs.map