Skip to content

Commit bf49e92

Browse files
refactor(utils): extract duplicated filter matching logic into parseFilterOption
Co-authored-by: cmuench <211294+cmuench@users.noreply.github.com>
1 parent 887109e commit bf49e92

2 files changed

Lines changed: 144 additions & 64 deletions

File tree

lib/utils.js

Lines changed: 44 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,44 @@ export async function validatePaaSOrOnPrem() {
9191
}
9292
}
9393

94+
export function parseFilterOption(f) {
95+
const match = f.match(/^([\w\.]+)(?::([a-z!]+))?(?:(<=|>=|<|>|=|!=|!~|~|\?|!|@@)(.*))?$/);
96+
if (!match) return null;
97+
98+
const field = match[1];
99+
const opString = match[2];
100+
const shorthand = match[3];
101+
let value = match[4] || '';
102+
103+
let condition = 'eq';
104+
105+
if (opString) {
106+
condition = opString === '!in' ? 'nin' : opString;
107+
} else if (shorthand) {
108+
switch (shorthand) {
109+
case '>': condition = 'gt'; break;
110+
case '<': condition = 'lt'; break;
111+
case '>=': condition = 'gteq'; break;
112+
case '<=': condition = 'lteq'; break;
113+
case '!=': condition = 'neq'; break;
114+
case '~': condition = 'like'; break;
115+
case '!~': condition = 'nlike'; break;
116+
case '@@': condition = 'finset'; break;
117+
case '?': condition = 'null'; value = '1'; break;
118+
case '!': condition = 'notnull'; value = '1'; break;
119+
default: condition = 'eq';
120+
}
121+
} else {
122+
return null;
123+
}
124+
125+
if ((condition === 'like' || condition === 'nlike') && value.includes('*')) {
126+
value = value.replaceAll('*', '%');
127+
}
128+
129+
return { field, condition, value };
130+
}
131+
94132
export function buildSearchCriteria(options) {
95133
const params = {
96134
...buildPaginationCriteria(options)
@@ -111,39 +149,9 @@ export function buildSearchCriteria(options) {
111149
let orIndex = 0;
112150

113151
for (const f of orFilters) {
114-
let match = f.match(/^([\w\.]+)(?::([a-z!]+))?(?:(<=|>=|<|>|=|!=|!~|~|\?|!|@@)(.*))?$/);
115-
if (match) {
116-
const field = match[1];
117-
const opString = match[2];
118-
const shorthand = match[3];
119-
let value = match[4] || '';
120-
121-
let condition = 'eq';
122-
123-
if (opString) {
124-
condition = opString === '!in' ? 'nin' : opString;
125-
} else if (shorthand) {
126-
switch (shorthand) {
127-
case '>': condition = 'gt'; break;
128-
case '<': condition = 'lt'; break;
129-
case '>=': condition = 'gteq'; break;
130-
case '<=': condition = 'lteq'; break;
131-
case '!=': condition = 'neq'; break;
132-
case '~': condition = 'like'; break;
133-
case '!~': condition = 'nlike'; break;
134-
case '@@': condition = 'finset'; break;
135-
case '?': condition = 'null'; value = '1'; break;
136-
case '!': condition = 'notnull'; value = '1'; break;
137-
default: condition = 'eq';
138-
}
139-
} else {
140-
console.error(chalk.yellow(`Warning: Invalid filter format "${f}". Expected formats like "field=value", "field:like=value", etc.`));
141-
continue;
142-
}
143-
144-
if ((condition === 'like' || condition === 'nlike') && value.includes('*')) {
145-
value = value.replaceAll('*', '%');
146-
}
152+
const parsed = parseFilterOption(f);
153+
if (parsed) {
154+
const { field, condition, value } = parsed;
147155

148156
params[`searchCriteria[filter_groups][${filterIndex}][filters][${orIndex}][field]`] = field;
149157
params[`searchCriteria[filter_groups][${filterIndex}][filters][${orIndex}][value]`] = value;
@@ -246,37 +254,10 @@ export function applyLocalSearchCriteria(data, options) {
246254
const orFilters = group.split(/\s*\|\|\s*/);
247255
result = result.filter(item => {
248256
return orFilters.some(f => {
249-
const match = f.match(/^([\w\.]+)(?::([a-z!]+))?(?:(<=|>=|<|>|=|!=|!~|~|\?|!|@@)(.*))?$/);
250-
if (!match) return false;
251-
252-
const field = match[1];
253-
const opString = match[2];
254-
const shorthand = match[3];
255-
let value = match[4] || '';
256-
257-
let condition = 'eq';
258-
if (opString) {
259-
condition = opString === '!in' ? 'nin' : opString;
260-
} else if (shorthand) {
261-
switch (shorthand) {
262-
case '>': condition = 'gt'; break;
263-
case '<': condition = 'lt'; break;
264-
case '>=': condition = 'gteq'; break;
265-
case '<=': condition = 'lteq'; break;
266-
case '!=': condition = 'neq'; break;
267-
case '~': condition = 'like'; break;
268-
case '!~': condition = 'nlike'; break;
269-
case '@@': condition = 'finset'; break;
270-
case '?': condition = 'null'; value = '1'; break;
271-
case '!': condition = 'notnull'; value = '1'; break;
272-
default: condition = 'eq';
273-
}
274-
}
275-
276-
if ((condition === 'like' || condition === 'nlike') && value.includes('*')) {
277-
value = value.replaceAll('*', '%');
278-
}
257+
const parsed = parseFilterOption(f);
258+
if (!parsed) return false;
279259

260+
const { field, condition, value } = parsed;
280261
const itemValue = item[field];
281262
const strItemVal = String(itemValue || '').toLowerCase();
282263
const strVal = String(value).toLowerCase();

tests/utils.test.js

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,17 @@ jest.unstable_mockModule('../lib/config.js', () => ({
3030
loadConfig: jest.fn()
3131
}));
3232

33-
const { handleError, readInput, validateAdobeCommerce, validatePaaSOrOnPrem, printTable, buildSearchCriteria, buildSortCriteria, applyLocalSearchCriteria, getFormatHeaders } = await import('../lib/utils.js');
33+
const {
34+
handleError,
35+
readInput,
36+
validateAdobeCommerce,
37+
validatePaaSOrOnPrem,
38+
printTable,
39+
buildSearchCriteria,
40+
buildSortCriteria,
41+
applyLocalSearchCriteria,
42+
getFormatHeaders
43+
} = await import('../lib/utils.js');
3444
const fs = (await import('fs')).default;
3545
const os = (await import('os')).default;
3646
const configMod = await import('../lib/config.js');
@@ -632,3 +642,92 @@ describe('buildSortCriteria', () => {
632642
});
633643
});
634644
});
645+
646+
describe('applyLocalSearchCriteria', () => {
647+
const data = [
648+
{ id: 1, name: 'Product A', price: 10, status: 'enabled', category_ids: '1,2' },
649+
{ id: 2, name: 'Product B', price: 20, status: 'disabled', category_ids: '2,3' },
650+
{ id: 3, name: 'Service C', price: 15, status: 'enabled', category_ids: '1' },
651+
];
652+
653+
it('should filter by equality', () => {
654+
const result = applyLocalSearchCriteria(data, { filter: ['status=enabled'] });
655+
expect(result).toHaveLength(2);
656+
expect(result[0].id).toBe(1);
657+
expect(result[1].id).toBe(3);
658+
});
659+
660+
it('should filter by shorthand operators', () => {
661+
const result = applyLocalSearchCriteria(data, { filter: ['price>10'] });
662+
expect(result).toHaveLength(2);
663+
expect(result[0].id).toBe(2);
664+
expect(result[1].id).toBe(3);
665+
});
666+
667+
it('should filter by like with wildcards', () => {
668+
const result = applyLocalSearchCriteria(data, { filter: ['name~*Product*'] });
669+
expect(result).toHaveLength(2);
670+
expect(result[0].id).toBe(1);
671+
expect(result[1].id).toBe(2);
672+
});
673+
674+
it('should filter with OR logic using ||', () => {
675+
const result = applyLocalSearchCriteria(data, { filter: ['id=1 || id=2'] });
676+
expect(result).toHaveLength(2);
677+
expect(result.map(i => i.id)).toContain(1);
678+
expect(result.map(i => i.id)).toContain(2);
679+
});
680+
681+
it('should sort data ASC', () => {
682+
const result = applyLocalSearchCriteria(data, { sort: ['price:ASC'] });
683+
expect(result[0].id).toBe(1);
684+
expect(result[1].id).toBe(3);
685+
expect(result[2].id).toBe(2);
686+
});
687+
688+
it('should sort data DESC', () => {
689+
const result = applyLocalSearchCriteria(data, { sort: ['price:DESC'] });
690+
expect(result[0].id).toBe(2);
691+
expect(result[1].id).toBe(3);
692+
expect(result[2].id).toBe(1);
693+
});
694+
695+
it('should paginate data', () => {
696+
const result = applyLocalSearchCriteria(data, { page: 2, size: 1 });
697+
expect(result).toHaveLength(1);
698+
expect(result[0].id).toBe(2);
699+
});
700+
701+
it('should sort data using sortBy and sortOrder', () => {
702+
const result = applyLocalSearchCriteria(data, { sortBy: 'price', sortOrder: 'DESC' });
703+
expect(result[0].id).toBe(2);
704+
expect(result[1].id).toBe(3);
705+
expect(result[2].id).toBe(1);
706+
});
707+
708+
it('should handle in/nin operators', () => {
709+
const resIn = applyLocalSearchCriteria(data, { filter: ['id:in=1,3'] });
710+
expect(resIn).toHaveLength(2);
711+
expect(resIn.map(i => i.id)).toEqual([1, 3]);
712+
713+
const resNin = applyLocalSearchCriteria(data, { filter: ['id:nin=1,3'] });
714+
expect(resNin).toHaveLength(1);
715+
expect(resNin[0].id).toBe(2);
716+
});
717+
718+
it('should handle null/notnull operators', () => {
719+
const dataWithNull = [...data, { id: 4, name: null }];
720+
const resNull = applyLocalSearchCriteria(dataWithNull, { filter: ['name?'] });
721+
expect(resNull).toHaveLength(1);
722+
expect(resNull[0].id).toBe(4);
723+
724+
const resNotNull = applyLocalSearchCriteria(dataWithNull, { filter: ['name!'] });
725+
expect(resNotNull).toHaveLength(3);
726+
});
727+
728+
it('should handle finset operator', () => {
729+
const result = applyLocalSearchCriteria(data, { filter: ['category_ids@@3'] });
730+
expect(result).toHaveLength(1);
731+
expect(result[0].id).toBe(2);
732+
});
733+
});

0 commit comments

Comments
 (0)