import {
Adapter,
Condition,
ContextOf,
GroupOperator,
Operator,
OrderDirection,
QueryError,
Sequence,
SelectSelector,
Statement,
UnsupportedError,
ViewKind,
} from "@decaf-ts/core";
import {
MangoOperator,
MangoQuery,
MangoSelector,
ViewResponse,
} from "../types";
import { Model } from "@decaf-ts/decorator-validation";
import { translateOperators } from "./translate";
import { CouchDBKeys } from "../constants";
import {
CouchDBGroupOperator,
CouchDBOperator,
CouchDBQueryLimit,
} from "./constants";
import { DBKeys } from "@decaf-ts/db-decorators";
import type { Context } from "@decaf-ts/db-decorators";
import { Metadata } from "@decaf-ts/decoration";
import {
generateDesignDocName,
generateViewName,
findViewMetadata,
} from "../views/generator";
import { CouchDBViewMetadata } from "../views/types";
import { CouchDBAdapter } from "../adapter";
type CouchDBViewDescriptor = {
ddoc: string;
view: string;
options: Record<string, any>;
};
type CouchDBAggregateInfo =
| {
kind: ViewKind;
meta: CouchDBViewMetadata;
descriptor: CouchDBViewDescriptor;
countDistinct?: boolean;
}
| {
kind: "avg";
attribute: string;
sumDescriptor: CouchDBViewDescriptor;
countDescriptor: CouchDBViewDescriptor;
};
const escapeRegExp = (value: string): string =>
value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
function nextLexicographicString(value: string): string {
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: string) {
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();
*/
export class CouchDBStatement<
M extends Model,
A extends Adapter<any, any, MangoQuery, any>,
R,
> extends Statement<M, A, R, MangoQuery> {
protected manualAggregation?: CouchDBAggregateInfo;
protected attributeTypeCache: Map<string, string | undefined> = new Map();
constructor(adapter: A) {
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
*/
protected build(): MangoQuery {
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: MangoSelector = {};
selectors[CouchDBKeys.TABLE] = {};
selectors[CouchDBKeys.TABLE] = Model.tableName(this.fromSelector);
const query: MangoQuery = { selector: selectors };
if (this.selectSelector) query.fields = this.selectSelector as string[];
if (this.whereCondition) {
const condition: MangoSelector = this.parseCondition(
Condition.and(
this.whereCondition,
Condition.attribute<M>(CouchDBKeys.TABLE as keyof M).eq(
query.selector[CouchDBKeys.TABLE]
)
)
).selector;
const selectorKeys = Object.keys(condition) as MangoOperator[];
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] as MangoSelector
).reduce((accum: MangoSelector[], val: any) => {
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] as any[]));
else accum.push(val);
return accum;
}, []),
];
query.selector = condition;
break;
case CouchDBGroupOperator.OR: {
const s: Record<any, any> = {};
s[CouchDBGroupOperator.AND] = [
condition,
...Object.entries(query.selector).map(([key, val]) => {
const result: Record<any, any> = {};
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 || ({} as MangoSelector);
for (const [selectorKey, direction] of this.orderBySelectors) {
const selector = selectorKey as string;
const rec: Record<string, OrderDirection> = {};
rec[selector] = direction as OrderDirection;
(query.sort as Record<string, OrderDirection>[]).push(rec);
if (!query.selector[selector]) {
query.selector[selector] = {} as MangoSelector;
(query.selector[selector] as MangoSelector)[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 ${CouchDBQueryLimit}`
);
query.limit = CouchDBQueryLimit;
}
if (this.offsetSelector) query.skip = this.offsetSelector;
return query;
}
/**
* @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
*/
protected processRecord(
r: any,
pkAttr: keyof M,
sequenceType: "Number" | "BigInt" | undefined,
ctx: Context
) {
if (r[CouchDBKeys.ID]) {
const [, ...keyArgs] = r[CouchDBKeys.ID].split(CouchDBKeys.SEPARATOR);
const id = keyArgs.join("_");
return this.adapter.revert(
r,
this.fromSelector,
Sequence.parseValue(sequenceType, id),
undefined,
ctx
);
}
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
*/
override async raw<R>(rawInput: MangoQuery, ...args: any[]): Promise<R> {
const { ctx } = this.logCtx(args, this.raw);
const aggregator = (rawInput as any)?.aggregateInfo;
if ((rawInput as any)?.aggregate && aggregator) {
return this.executeAggregate<R>(aggregator, ctx);
}
const results: any[] = await this.adapter.raw(rawInput, true, ctx);
const pkAttr = Model.pk(this.fromSelector);
const type = Metadata.get(
this.fromSelector,
Metadata.key(DBKeys.ID, pkAttr as string)
)?.type;
const processed = results.map((r) =>
this.processRecord(r, pkAttr, type, ctx)
);
if (this.manualAggregation) {
const manualResult = this.executeManualAggregation<R>(
processed,
this.manualAggregation,
ctx
);
this.manualAggregation = undefined;
return manualResult;
}
if (!this.selectSelector && this.groupBySelectors?.length) {
return this.groupSelectResults(processed) as R;
}
if (!this.selectSelector) return processed as R;
return results as R;
}
/**
* @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
*/
protected buildAggregateInfo(): CouchDBAggregateInfo | undefined {
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;
}
protected createAggregateDescriptor(
kind: ViewKind,
attribute?: string
): Extract<CouchDBAggregateInfo, { kind: ViewKind }> | undefined {
if (!this.fromSelector) return undefined;
const metas = findViewMetadata(this.fromSelector, kind, attribute);
if (!metas.length) return undefined;
const meta = metas[0];
const tableName = Model.tableName(this.fromSelector);
const viewName = generateViewName(tableName, meta.attribute, kind, meta);
const ddoc = meta.ddoc || generateDesignDocName(tableName, viewName);
const options: Record<string, any> = {
reduce: meta.reduce !== undefined ? true : !meta.returnDocs,
};
if (kind === "distinct" || kind === "groupBy") options.group = true;
return {
kind,
meta,
descriptor: {
ddoc,
view: viewName,
options,
},
};
}
protected createAggregateQuery(
info: CouchDBAggregateInfo
): MangoQuery & { aggregate: true; aggregateInfo: CouchDBAggregateInfo } {
return {
selector: {},
aggregate: true,
aggregateInfo: info,
} as MangoQuery & { aggregate: true; aggregateInfo: CouchDBAggregateInfo };
}
protected shouldUseManualAggregation(): boolean {
return !!this.whereCondition;
}
protected async executeAggregate<R>(
info: CouchDBAggregateInfo,
ctx: ContextOf<A>
): Promise<R> {
if (!this.isViewAggregate(info)) {
return this.handleAverage<R>(info, ctx);
}
const couchAdapter = this.getCouchAdapter();
const viewInfo = info as Extract<CouchDBAggregateInfo, { kind: ViewKind }>;
const response = await couchAdapter.view<ViewResponse>(
viewInfo.descriptor.ddoc,
viewInfo.descriptor.view,
viewInfo.descriptor.options,
ctx
);
return this.processViewResponse<R>(info, response);
}
protected async handleAverage<R>(
info: CouchDBAggregateInfo,
ctx: ContextOf<A>
): Promise<R> {
if (info.kind !== "avg")
throw new 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<ViewResponse>(
sumDesc.ddoc,
sumDesc.view,
sumDesc.options,
ctx
),
couchAdapter.view<ViewResponse>(
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 as unknown as R;
return (sum / count) as unknown as R;
}
protected executeManualAggregation<R>(
docs: any[],
info: CouchDBAggregateInfo,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
ctx: ContextOf<A>
): R {
if (!this.fromSelector)
throw new QueryError("Manual aggregation requires a target model");
if (info.kind === "avg") {
return this.computeAverage<R>(docs, info.attribute) as R;
}
if (info.kind === "groupBy") {
return this.computeGroupBy<R>(docs, info.meta.attribute) as R;
}
const attribute = info.meta.attribute;
switch (info.kind) {
case "count": {
if (info.countDistinct) {
return this.computeDistinctCount<R>(docs, attribute) as R;
}
return this.computeCount<R>(docs, attribute) as R;
}
case "distinct":
if (info.countDistinct) {
return this.computeDistinctCount<R>(docs, attribute) as R;
}
return this.computeDistinctValues<R>(docs, attribute) as R;
case "sum":
return this.computeSum<R>(docs, attribute) as R;
case "min":
return this.computeMinMax<R>(docs, attribute, "min") as R;
case "max":
return this.computeMinMax<R>(docs, attribute, "max") as R;
default:
throw new QueryError(`Unsupported manual aggregation ${info.kind}`);
}
}
protected computeCount<R>(docs: any[], attribute?: string): R {
if (!attribute) return docs.length as unknown as R;
const values = this.collectValues(docs, attribute);
return values.filter((value) => value !== undefined && value !== null)
.length as R;
}
protected computeDistinctCount<R>(docs: any[], attribute?: string): R {
const values = attribute
? this.collectValues(docs, attribute).filter(
(value) => value !== undefined && value !== null
)
: docs;
const seen = new Set<string>();
values.forEach((value) => seen.add(JSON.stringify(value)));
return seen.size as unknown as R;
}
protected computeDistinctValues<R>(docs: any[], attribute?: string): R {
if (!attribute) return [] as unknown as R;
const values = this.collectValues(docs, attribute);
const seen = new Set<string>();
const unique: any[] = [];
values.forEach((value) => {
const key = JSON.stringify(value);
if (!seen.has(key)) {
seen.add(key);
unique.push(value);
}
});
return unique as unknown as R;
}
protected computeSum<R>(docs: any[], attribute?: string): R {
if (!attribute) return docs.length as unknown as R;
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 as unknown as R;
}
protected computeAverage<R>(docs: any[], attribute?: string): R {
if (!attribute) return 0 as unknown as R;
const values = this.collectValues(docs, attribute).filter(
(value) => value !== undefined && value !== null
);
if (!values.length) return 0 as unknown as R;
const sum = values.reduce(
(acc, value) =>
acc + this.toNumericValue(value, attribute, "AVG operation"),
0
);
return (sum / values.length) as unknown as R;
}
protected computeMinMax<R>(
docs: any[],
attribute: string | undefined,
mode: "min" | "max"
): R {
if (!attribute) return null as unknown as R;
const values = this.collectValues(docs, attribute).filter(
(value) => value !== undefined && value !== null
);
let currentValue: any = null;
let currentComparable: number | null = 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 as unknown as R;
}
protected computeGroupBy<R>(docs: any[], attribute: string): R {
const grouped: Record<string, any[]> = {};
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 as unknown as R;
}
protected groupSelectResults(docs: any[]): Record<string, any[]> {
if (!this.groupBySelectors?.length) return {};
const attribute = this.resolveSelectorAttribute(this.groupBySelectors[0]);
if (!attribute) return {};
const grouped: Record<string, any[]> = {};
docs.forEach((doc) => {
const key = this.groupKey(
this.convertValueByAttribute(attribute, doc[attribute])
);
if (!grouped[key]) grouped[key] = [];
grouped[key].push(doc);
});
return grouped;
}
protected collectValues(docs: any[], attribute: string): any[] {
return docs.map((doc) => {
if (!doc || typeof doc !== "object") return undefined;
return this.convertValueByAttribute(attribute, doc[attribute]);
});
}
protected convertValueByAttribute(attribute: string, value: any): any {
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;
}
protected getAttributeType(attribute?: string): string | undefined {
if (!attribute || !this.fromSelector) return undefined;
if (this.attributeTypeCache.has(attribute)) {
return this.attributeTypeCache.get(attribute);
}
const metaType =
Metadata.type(this.fromSelector, attribute as any) ??
(Metadata as any).getPropDesignTypes?.(this.fromSelector, attribute)
?.designType;
const normalized = this.normalizeMetaType(metaType);
this.attributeTypeCache.set(attribute, normalized);
return normalized;
}
protected normalizeMetaType(metaType: any): string | undefined {
if (!metaType) return undefined;
if (typeof metaType === "string") return metaType.toLowerCase();
if (typeof metaType === "function" && metaType.name)
return metaType.name.toLowerCase();
return undefined;
}
protected normalizeComparable(value: any): number | null {
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;
}
protected groupKey(value: any): string {
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);
}
protected toNumericValue(value: any, field: string, context: string): number {
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 QueryError(
`${context} on "${field}" requires numeric values, but got ${typeof value}`
);
}
protected convertAggregateValue(
attribute: string | undefined,
value: any
): any {
if (!attribute) return value;
return this.convertValueByAttribute(attribute, value);
}
protected resolveSelectorAttribute(
selector?: SelectSelector<M> | null
): string | undefined {
if (selector == null) return undefined;
return String(selector);
}
protected missingDecorator(
kind: ViewKind | "avg",
attribute?: string
): UnsupportedError {
const decorator = this.decoratorForKind(kind);
const table = this.fromSelector
? Model.tableName(this.fromSelector)
: "<unknown table>";
const attributeDesc = attribute ? ` on "${attribute}"` : "";
return new UnsupportedError(
`${decorator} decorator is required for CouchDB ${kind} aggregation${attributeDesc} on table "${table}".`
);
}
protected decoratorForKind(kind: ViewKind | "avg"): string {
const map: Record<string, string> = {
count: "@count",
sum: "@sum",
min: "@min",
max: "@max",
distinct: "@distinct",
groupBy: "@groupBy",
view: "@view",
avg: "@avg",
};
return map[kind] || `@${kind}`;
}
protected processViewResponse<R>(
info: CouchDBAggregateInfo,
response: ViewResponse
): R {
if (info.kind === "avg")
throw new QueryError(
"Average results should be handled before processing rows"
);
const rows = response.rows || [];
const viewInfo = info as Extract<CouchDBAggregateInfo, { kind: ViewKind }>;
const meta = viewInfo.meta;
if (viewInfo.countDistinct) {
return (rows.length || 0) as unknown as R;
}
if (viewInfo.kind === "distinct" || viewInfo.kind === "groupBy") {
return rows.map((row) =>
this.convertAggregateValue(
viewInfo.meta.attribute,
row.key ?? row.value
)
) as unknown as R;
}
if (meta.returnDocs) {
return rows.map((row) => row.value ?? row.doc ?? row) as unknown as R;
}
if (!rows.length) {
return (viewInfo.kind === "count" ? 0 : null) as unknown as R;
}
return this.convertAggregateValue(
viewInfo.meta.attribute,
rows[0].value ?? rows[0].key ?? null
) as unknown as R;
}
protected isViewAggregate(
info: CouchDBAggregateInfo
): info is Extract<CouchDBAggregateInfo, { kind: ViewKind }> {
return info.kind !== "avg";
}
protected getCouchAdapter(): CouchDBAdapter<any, any, any> {
return this.adapter as unknown as CouchDBAdapter<any, any, any>;
}
protected parseCondition(condition: Condition<M>): MangoQuery {
/**
* @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: MangoOperator,
obj1: MangoSelector,
obj2: MangoSelector
): MangoQuery {
const result: MangoQuery = { selector: {} as MangoSelector };
result.selector[op] = [obj1, obj2];
return result;
}
const { attr1, operator, comparison } = condition as unknown as {
attr1: string | Condition<M>;
operator: Operator | GroupOperator;
comparison: any;
};
if (operator === Operator.STARTS_WITH) {
if (typeof attr1 !== "string")
throw new QueryError("STARTS_WITH requires an attribute name");
if (typeof comparison !== "string")
throw new QueryError("STARTS_WITH requires a string comparison");
const range = prefixRange(comparison);
return {
selector: {
[attr1]: {
[CouchDBOperator.BIGGER_EQ]: range.start,
[CouchDBOperator.SMALLER]: range.end,
},
},
};
}
if (operator === Operator.ENDS_WITH) {
if (typeof attr1 !== "string")
throw new QueryError("ENDS_WITH requires an attribute name");
if (typeof comparison !== "string")
throw new QueryError("ENDS_WITH requires a string comparison");
return {
selector: {
[attr1]: {
[CouchDBOperator.REGEXP]: `${escapeRegExp(comparison)}$`,
},
},
};
}
if (operator === Operator.BETWEEN) {
const attr = attr1 as string;
if (!Array.isArray(comparison) || comparison.length !== 2)
throw new QueryError("BETWEEN operator requires [min, max] comparison");
const [min, max] = comparison;
const opBetween: MangoSelector = {} as MangoSelector;
opBetween[attr] = {} as MangoSelector;
(opBetween[attr] as MangoSelector)[
translateOperators(Operator.BIGGER_EQ)
] = min;
(opBetween[attr] as MangoSelector)[
translateOperators(Operator.SMALLER_EQ)
] = max;
return { selector: opBetween };
}
let op: MangoSelector = {} as MangoSelector;
if (
[GroupOperator.AND, GroupOperator.OR, Operator.NOT].indexOf(
operator as GroupOperator
) === -1
) {
op[attr1 as string] = {} as MangoSelector;
(op[attr1 as string] as MangoSelector)[translateOperators(operator)] =
comparison;
} else if (operator === Operator.NOT) {
op = this.parseCondition(attr1 as Condition<M>).selector as MangoSelector;
op[translateOperators(Operator.NOT)] = {} as MangoSelector;
(op[translateOperators(Operator.NOT)] as MangoSelector)[
(attr1 as unknown as { attr1: string }).attr1
] = comparison;
} else {
const op1: any = this.parseCondition(attr1 as Condition<M>).selector;
const op2: any = this.parseCondition(comparison as Condition<M>).selector;
op = merge(translateOperators(operator), op1, op2).selector;
}
return { selector: op };
}
}
Source