@decaf-ts/for-couchdb
Version:
decaf-ts couchdb wrappers
868 lines • 37 kB
JavaScript
"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