Skip to content
6 changes: 4 additions & 2 deletions packages/react-querybuilder/src/types/importExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,17 @@ export interface FormatQueryOptions {

export type ValueProcessorOptions = Pick<FormatQueryOptions, 'parseNumbers'>;

export type ValueProcessorInternal = (rule: RuleType, options: ValueProcessorOptions) => string;
export type ValueProcessorByRule = (rule: RuleType, options?: ValueProcessorOptions) => string;

export type ValueProcessor = (
export type ValueProcessorLegacy = (
field: string,
operator: string,
value: any,
valueSource?: ValueSource
) => string;

export type ValueProcessor = ValueProcessorLegacy | ValueProcessorByRule;

export interface ParameterizedSQL {
sql: string;
params: any[];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { JsonLogicVar } from 'json-logic-js';
import type { FormatQueryOptions, RQBJsonLogic, RuleType } from '../../types/index.noReact';
import type { RQBJsonLogic, RuleType, ValueProcessorOptions } from '../../types/index.noReact';
import { isValidValue, shouldRenderAsNumber, toArray } from './utils';

const convertOperator = (op: '<' | '<=' | '=' | '!=' | '>' | '>=') =>
Expand All @@ -11,9 +11,9 @@ const convertOperator = (op: '<' | '<=' | '=' | '!=' | '>' | '>=') =>
const negateIfNotOp = (op: string, jsonRule: RQBJsonLogic) =>
/^(does)?not/i.test(op) ? { '!': jsonRule } : jsonRule;

export const internalRuleProcessorJSONLogic = (
export const defaultRuleProcessorJsonLogic = (
{ field, operator, value, valueSource }: RuleType,
{ parseNumbers }: Pick<FormatQueryOptions, 'parseNumbers'>
{ parseNumbers }: ValueProcessorOptions
): RQBJsonLogic => {
const valueIsField = valueSource === 'field';
const fieldObject: JsonLogicVar = { var: field };
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { ValueProcessorInternal } from '../../types/index.noReact';
import type { ValueProcessorByRule } from '../../types/index.noReact';
import { isValidValue, shouldRenderAsNumber, toArray, trimIfString } from './utils';

export const internalValueProcessor: ValueProcessorInternal = (
export const defaultValueProcessorByRule: ValueProcessorByRule = (
{ operator, value, valueSource },
{ parseNumbers }
// istanbul ignore next
{ parseNumbers } = {}
) => {
const valueIsField = valueSource === 'field';
const operatorLowerCase = operator.toLowerCase();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { ValueProcessorInternal } from '../../types/index.noReact';
import type { ValueProcessorByRule } from '../../types/index.noReact';
import { shouldRenderAsNumber, toArray, trimIfString } from './utils';

const shouldNegate = (op: string) => /^(does)?not/i.test(op);

export const internalValueProcessorCEL: ValueProcessorInternal = (
export const defaultValueProcessorCELByRule: ValueProcessorByRule = (
{ field, operator, value, valueSource },
{ parseNumbers }
// istanbul ignore next
{ parseNumbers } = {}
) => {
const valueIsField = valueSource === 'field';
const operatorTL = operator.replace(/^=$/, '==');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { ValueProcessorInternal } from '../../types/index.noReact';
import type { ValueProcessorByRule } from '../../types/index.noReact';
import { isValidValue, mongoOperators, shouldRenderAsNumber, toArray, trimIfString } from './utils';

export const internalValueProcessorMongoDB: ValueProcessorInternal = (
export const defaultValueProcessorMongoDBByRule: ValueProcessorByRule = (
{ field, operator, value, valueSource },
{ parseNumbers }
// istanbul ignore next
{ parseNumbers } = {}
) => {
const valueIsField = valueSource === 'field';
const useBareValue =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import type { ValueProcessorInternal } from '../../types/index.noReact';
import type { ValueProcessorByRule } from '../../types/index.noReact';
import { shouldRenderAsNumber, toArray, trimIfString } from './utils';

const shouldNegate = (op: string) => /^(does)?not/i.test(op);

const wrapInNegation = (clause: string, negate: boolean) =>
`${negate ? '!(' : ''}${clause}${negate ? ')' : ''}`;

export const internalValueProcessorSpEL: ValueProcessorInternal = (
export const defaultValueProcessorSpELByRule: ValueProcessorByRule = (
{ field, operator, value, valueSource },
{ parseNumbers }
// istanbul ignore next
{ parseNumbers } = {}
) => {
const valueIsField = valueSource === 'field';
const operatorTL = operator.replace(/^=$/, '==');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import {
defaultValueProcessor,
} from '.';
import { defaultPlaceholderFieldName, defaultPlaceholderOperatorName } from '../../defaults';
import type { RuleGroupType, RuleGroupTypeIC, ValueProcessor } from '../../types/index.noReact';
import type {
RuleGroupType,
RuleGroupTypeIC,
ValueProcessorByRule,
ValueProcessorLegacy,
} from '../../types/index.noReact';
import { convertToIC } from '../convertQuery';
import { add } from '../queryTools';
import { formatQuery } from './formatQuery';
Expand Down Expand Up @@ -819,7 +824,7 @@ it('handles invalid type correctly', () => {
expect(formatQuery(query, 'null')).toBe('');
});

it('handles custom valueProcessor correctly', () => {
it('handles custom valueProcessors correctly', () => {
const queryWithArrayValue: RuleGroupType = {
id: 'g-root',
combinator: 'and',
Expand All @@ -838,17 +843,31 @@ it('handles custom valueProcessor correctly', () => {
not: false,
};

const valueProcessor: ValueProcessor = (_field, operator, value) => {
const valueProcessorLegacy: ValueProcessorLegacy = (_field, operator, value) => {
if (operator === 'in') {
return `(${value.map((v: string) => `'${v.trim()}'`).join(', /* and */ ')})`;
} else {
return `'${value}'`;
}
};

expect(formatQuery(queryWithArrayValue, { format: 'sql', valueProcessor })).toBe(
`(instrument in ('Guitar', /* and */ 'Vocals') and lastName = 'Vai')`
);
expect(
formatQuery(queryWithArrayValue, { format: 'sql', valueProcessor: valueProcessorLegacy })
).toBe(`(instrument in ('Guitar', /* and */ 'Vocals') and lastName = 'Vai')`);

const queryForNewValueProcessor: RuleGroupType = {
combinator: 'and',
rules: [{ field: 'f1', operator: '=', value: 'v1', valueSource: 'value' }],
};

const valueProcessor: ValueProcessorByRule = (
{ field, operator, value, valueSource },
{ parseNumbers } = {}
) => `${field}-${operator}-${value}-${valueSource}-${!!parseNumbers}`;

expect(
formatQuery(queryForNewValueProcessor, { format: 'sql', parseNumbers: true, valueProcessor })
).toBe('(f1 = f1-=-v1-value-true)');
});

it('handles quoteFieldNamesWith correctly', () => {
Expand Down
34 changes: 19 additions & 15 deletions packages/react-querybuilder/src/utils/formatQuery/formatQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ import type {
} from '../../types/index.noReact';
import { convertFromIC } from '../convertQuery';
import { isRuleOrGroupValid } from '../isRuleOrGroupValid';
import { internalRuleProcessorJSONLogic } from './internalRuleProcessorJSONLogic';
import { internalValueProcessor } from './internalValueProcessor';
import { internalValueProcessorCEL } from './internalValueProcessorCEL';
import { internalValueProcessorMongoDB } from './internalValueProcessorMongoDB';
import { internalValueProcessorSpEL } from './internalValueProcessorSpEL';
import { defaultRuleProcessorJsonLogic } from './defaultRuleProcessorJsonLogic';
import { defaultValueProcessorByRule } from './defaultValueProcessorByRule';
import { defaultValueProcessorCELByRule } from './defaultValueProcessorCELByRule';
import { defaultValueProcessorMongoDBByRule } from './defaultValueProcessorMongoDBByRule';
import { defaultValueProcessorSpELByRule } from './defaultValueProcessorSpELByRule';
import {
celCombinatorMap,
isValueProcessorLegacy,
mapSQLOperator,
numerifyValues,
shouldRenderAsNumber,
Expand Down Expand Up @@ -64,7 +65,7 @@ function formatQuery(
): string;
function formatQuery(ruleGroup: RuleGroupTypeAny, options: FormatQueryOptions | ExportFormat = {}) {
let format: ExportFormat = 'json';
let valueProcessorInternal = internalValueProcessor;
let valueProcessorInternal = defaultValueProcessorByRule;
let quoteFieldNamesWith = '';
let validator: QueryValidator = () => true;
let fields: Required<FormatQueryOptions>['fields'] = [];
Expand All @@ -78,25 +79,28 @@ function formatQuery(ruleGroup: RuleGroupTypeAny, options: FormatQueryOptions |
if (typeof options === 'string') {
format = options.toLowerCase() as ExportFormat;
if (format === 'mongodb') {
valueProcessorInternal = internalValueProcessorMongoDB;
valueProcessorInternal = defaultValueProcessorMongoDBByRule;
} else if (format === 'cel') {
valueProcessorInternal = internalValueProcessorCEL;
valueProcessorInternal = defaultValueProcessorCELByRule;
} else if (format === 'spel') {
valueProcessorInternal = internalValueProcessorSpEL;
valueProcessorInternal = defaultValueProcessorSpELByRule;
}
} else {
format = (options.format ?? 'json').toLowerCase() as ExportFormat;
const { valueProcessor = null } = options;
valueProcessorInternal =
typeof valueProcessor === 'function'
? r => valueProcessor(r.field, r.operator, r.value, r.valueSource)
? r =>
isValueProcessorLegacy(valueProcessor)
? valueProcessor(r.field, r.operator, r.value, r.valueSource)
: valueProcessor(r, { parseNumbers })
: format === 'mongodb'
? internalValueProcessorMongoDB
? defaultValueProcessorMongoDBByRule
: format === 'cel'
? internalValueProcessorCEL
? defaultValueProcessorCELByRule
: format === 'spel'
? internalValueProcessorSpEL
: internalValueProcessor;
? defaultValueProcessorSpELByRule
: defaultValueProcessorByRule;
quoteFieldNamesWith = options.quoteFieldNamesWith ?? '';
validator = options.validator ?? (() => true);
fields = options.fields ?? [];
Expand Down Expand Up @@ -444,7 +448,7 @@ function formatQuery(ruleGroup: RuleGroupTypeAny, options: FormatQueryOptions |
) {
return false;
}
return internalRuleProcessorJSONLogic(rule, { parseNumbers });
return defaultRuleProcessorJsonLogic(rule, { parseNumbers });
})
.filter(Boolean);

Expand Down
20 changes: 12 additions & 8 deletions packages/react-querybuilder/src/utils/formatQuery/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { ValueProcessor } from '../../types/index.noReact';
import { internalValueProcessor } from './internalValueProcessor';
import { internalValueProcessorCEL } from './internalValueProcessorCEL';
import { internalValueProcessorMongoDB } from './internalValueProcessorMongoDB';
import { internalValueProcessorSpEL } from './internalValueProcessorSpEL';
import { defaultValueProcessorByRule } from './defaultValueProcessorByRule';
import { defaultValueProcessorCELByRule } from './defaultValueProcessorCELByRule';
import { defaultValueProcessorMongoDBByRule } from './defaultValueProcessorMongoDBByRule';
import { defaultValueProcessorSpELByRule } from './defaultValueProcessorSpELByRule';

const internalValueProcessors = {
default: internalValueProcessor,
mongodb: internalValueProcessorMongoDB,
cel: internalValueProcessorCEL,
spel: internalValueProcessorSpEL,
default: defaultValueProcessorByRule,
mongodb: defaultValueProcessorMongoDBByRule,
cel: defaultValueProcessorCELByRule,
spel: defaultValueProcessorSpELByRule,
} as const;

const generateValueProcessor =
Expand All @@ -24,3 +24,7 @@ export const defaultMongoDBValueProcessor = generateValueProcessor('mongodb');
export const defaultCELValueProcessor = generateValueProcessor('cel');
export const defaultSpELValueProcessor = generateValueProcessor('spel');
export * from './formatQuery';
export { defaultValueProcessorByRule };
export { defaultValueProcessorCELByRule };
export { defaultValueProcessorMongoDBByRule };
export { defaultValueProcessorSpELByRule };
10 changes: 9 additions & 1 deletion packages/react-querybuilder/src/utils/formatQuery/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { DefaultCombinatorName, RuleGroupTypeAny } from '../../types/index.noReact';
import type {
DefaultCombinatorName,
RuleGroupTypeAny,
ValueProcessor,
ValueProcessorLegacy,
} from '../../types/index.noReact';

export const numericRegex = /^\s*[+-]?(\d+|\d*\.\d+|\d+\.\d*)([Ee][+-]?\d+)?\s*$/;

Expand Down Expand Up @@ -104,3 +109,6 @@ export const shouldRenderAsNumber = (v: any, parseNumbers?: boolean) =>
(typeof v === 'number' ||
typeof v === 'bigint' ||
(typeof v === 'string' && numericRegex.test(v)));

export const isValueProcessorLegacy = (vp: ValueProcessor): vp is ValueProcessorLegacy =>
vp.length >= 3;