import { Comparators, Comparison, Operation, Operators, StructuredQuery, } from "@langchain/core/structured_query"; /** * Utility class used to duplicate parameters for a proxy object, * specifically designed to work with `SupabaseFilter` objects. It * contains methods to handle different types of operations such as "or", * "filter", "in", "contains", "textSearch", "match", "not", and default * operations. */ export class ProxyParamsDuplicator { constructor() { Object.defineProperty(this, "duplicationAllowedOps", { enumerable: true, configurable: true, writable: true, value: [ "eq", "neq", "lt", "lte", "gt", "gte", "like", "ilike", "or", "in", "contains", "match", "not", "textSearch", "filter", ] }); Object.defineProperty(this, "values", { enumerable: true, configurable: true, writable: true, value: [] }); } /** * Creates a proxy handler for a `SupabaseFilter` object. The handler * intercepts get operations and applies specific logic based on the * property being accessed. * @returns A proxy handler for a `SupabaseFilter` object. */ buildProxyHandler() { const proxyHandler = { get: (target, prop, receiver) => { if (typeof target[prop] === "function") { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (...args) => { if (this.duplicationAllowedOps.includes(String(prop))) { switch (String(prop)) { case "or": // args[0]: filters, args[1]: { foreignTable } this.addOrClause(args[0], args[1]); break; case "filter": // args[0]: column, args[1]: operator, args[2]: value this.addFilterClause(args[0], args[1], args[2]); break; case "in": // args[0]: column, args[1]: values this.addInClause(args[0], args[1]); break; case "contains": // args[0]: column, args[1]: value this.addContainsClause(args[0], args[1]); break; case "textSearch": // args[0]: column, args[1]: query, args[2]: { config, type } this.addTextSearchClause(args[0], args[1], args[2]); break; case "match": // args[0]: query this.addMatchClause(args[0]); break; case "not": // args[0]: column, args[1]: operator, args[2]: value this.addNotClause(args[0], args[1], args[2]); break; default: // args[0]: column, args[1]: value this.addDefaultOpClause(prop, args[0], args[1]); } return new Proxy(target, proxyHandler); } else { throw new Error("Filter operation not supported for 'or' mergeFiltersOperator"); } }; } else { return Reflect.get(target, prop, receiver); } }, }; return proxyHandler; } /** * Removes type annotations from a value string. * @param value The value string to clean. * @returns The cleaned value string. */ removeType(value) { let cleanedValue = value; if (cleanedValue.includes("::float")) { cleanedValue = cleanedValue.replace("::float", ""); } if (cleanedValue.includes("::int")) { cleanedValue = cleanedValue.replace("::int", ""); } return cleanedValue; } /** * Adds a default operation clause to the values array. * @param prop The operation property. * @param column The column to apply the operation to. * @param value The value for the operation. */ addDefaultOpClause(prop, column, value) { this.values.push([this.removeType(column), `${String(prop)}.${value}`]); } /** * Adds an 'or' clause to the values array. * @param filters The filters for the 'or' clause. * @param foreignTable Optional foreign table for the 'or' clause. */ addOrClause(filters, { foreignTable } = {}) { const key = foreignTable ? `${foreignTable}.or` : "or"; this.values.push([this.removeType(key), `(${filters})`]); } /** * Adds a 'filter' clause to the values array. * @param column The column to apply the filter to. * @param operator The operator for the filter. * @param value The value for the filter. */ addFilterClause(column, operator, value) { this.values.push([this.removeType(column), `${operator}.${value}`]); } /** * Adds an 'in' clause to the values array. * @param column The column to apply the 'in' clause to. * @param values The values for the 'in' clause. */ addInClause(column, values) { const cleanedValues = values .map((s) => { if (typeof s === "string" && /[,()]/.test(s)) return `"${s}"`; else return `${s}`; }) .join(","); this.values.push([this.removeType(column), `in.(${cleanedValues})`]); } /** * Adds a 'contains' clause to the values array. * @param column The column to apply the 'contains' clause to. * @param value The value for the 'contains' clause. */ addContainsClause(column, value) { if (typeof value === "string") { this.values.push([this.removeType(column), `cs.${value}`]); } else if (Array.isArray(value)) { this.values.push([this.removeType(column), `cs.{${value.join(",")}}`]); } else { this.values.push([ this.removeType(column), `cs.${JSON.stringify(value)}`, ]); } } /** * Adds a 'textSearch' clause to the values array. * @param column The column to apply the 'textSearch' clause to. * @param query The query for the 'textSearch' clause. * @param config Optional configuration for the 'textSearch' clause. * @param type Optional type for the 'textSearch' clause. */ addTextSearchClause(column, query, { config, type, } = {}) { let typePart = ""; if (type === "plain") { typePart = "pl"; } else if (type === "phrase") { typePart = "ph"; } else if (type === "websearch") { typePart = "w"; } const configPart = config === undefined ? "" : `(${config})`; this.values.push([ this.removeType(column), `${typePart}fts${configPart}.${query}`, ]); } /** * Adds a 'not' clause to the values array. * @param column The column to apply the 'not' clause to. * @param operator The operator for the 'not' clause. * @param value The value for the 'not' clause. */ addNotClause(column, operator, value) { this.values.push([column, `not.${operator}.${value}`]); } /** * Adds a 'match' clause to the values array. * @param query The query for the 'match' clause. */ addMatchClause(query) { Object.entries(query).forEach(([column, value]) => { this.values.push([column, `eq.${value}`]); }); } /** * Returns the flattened parameters as a string. * @returns The flattened parameters as a string. */ flattenedParams() { const mapped = this.values.map(([k, v]) => `${k}.${v}`); if (mapped.length === 1) return mapped[0]; return `and(${mapped.join(",")})`; } /** * Gets flattened parameters from a `SupabaseFilter` and a * `SupabaseFilterRPCCall`. * @param rpc The `SupabaseFilter` object. * @param filter The `SupabaseFilterRPCCall` object. * @returns The flattened parameters as a string. */ static getFlattenedParams(rpc, filter) { const proxiedDuplicator = new ProxyParamsDuplicator(); const proxiedRpc = new Proxy(rpc, proxiedDuplicator.buildProxyHandler()); void filter(proxiedRpc); return proxiedDuplicator.flattenedParams(); } } /** * Converts a `SupabaseMetadata` object into a `StructuredQuery` object. * The function creates a new `StructuredQuery` object and uses the * `Operation` and `Comparison` classes to build the query. */ export function convertObjectFilterToStructuredQuery(objFilter) { return new StructuredQuery("", new Operation(Operators.and, Object.entries(objFilter).map(([column, value]) => new Comparison(Comparators.eq, column, value)))); }