store-scrapper-js-common 1.0.209 → 1.0.215
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/enums/fetch-type.enum.d.ts +1 -0
- package/dist/enums/fetch-type.enum.js +1 -0
- package/dist/enums/fetch-type.enum.js.map +1 -1
- package/dist/utils/eval-filter.d.ts +11 -0
- package/dist/utils/eval-filter.js +68 -0
- package/dist/utils/eval-filter.js.map +1 -0
- package/dist/utils/eval-filter.spec.d.ts +1 -0
- package/dist/utils/eval-filter.spec.js +190 -0
- package/dist/utils/eval-filter.spec.js.map +1 -0
- package/dist/utils/index.d.ts +3 -1
- package/dist/utils/index.js +3 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/enums/fetch-type.enum.ts +2 -0
- package/src/utils/eval-filter.spec.ts +251 -0
- package/src/utils/eval-filter.ts +100 -0
- package/src/utils/index.ts +4 -0
|
@@ -9,6 +9,7 @@ export declare enum FetchTypeEnum {
|
|
|
9
9
|
PUPPETEER_EXTERNAL_HOST_AS_JSON = "puppeteer_external_host_as_json",
|
|
10
10
|
EXTERNAL_HTTP_SERVICE = "external_http_service",
|
|
11
11
|
CURL_IMPERSONATE = "curl_impersonate",
|
|
12
|
+
CLOAK_BROWSER = "cloak_browser",
|
|
12
13
|
CURL = "curl",
|
|
13
14
|
FLARESOLVERR = "flaresolverr"
|
|
14
15
|
}
|
|
@@ -13,6 +13,7 @@ var FetchTypeEnum;
|
|
|
13
13
|
FetchTypeEnum["PUPPETEER_EXTERNAL_HOST_AS_JSON"] = "puppeteer_external_host_as_json";
|
|
14
14
|
FetchTypeEnum["EXTERNAL_HTTP_SERVICE"] = "external_http_service";
|
|
15
15
|
FetchTypeEnum["CURL_IMPERSONATE"] = "curl_impersonate";
|
|
16
|
+
FetchTypeEnum["CLOAK_BROWSER"] = "cloak_browser";
|
|
16
17
|
FetchTypeEnum["CURL"] = "curl";
|
|
17
18
|
FetchTypeEnum["FLARESOLVERR"] = "flaresolverr";
|
|
18
19
|
})(FetchTypeEnum || (exports.FetchTypeEnum = FetchTypeEnum = {}));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch-type.enum.js","sourceRoot":"/","sources":["enums/fetch-type.enum.ts"],"names":[],"mappings":";;;AAAA,IAAY,
|
|
1
|
+
{"version":3,"file":"fetch-type.enum.js","sourceRoot":"/","sources":["enums/fetch-type.enum.ts"],"names":[],"mappings":";;;AAAA,IAAY,aAmBX;AAnBD,WAAY,aAAa;IACrB,gCAAe,CAAA;IACf,wCAAuB,CAAA;IACvB,0DAAyC,CAAA;IACzC,0EAAyD,CAAA;IACzD,wCAAuB,CAAA;IACvB,wDAAuC,CAAA;IACvC,oEAAmD,CAAA;IACnD,oFAAmE,CAAA;IAEnE,gEAA+C,CAAA;IAE/C,sDAAqC,CAAA;IAErC,gDAA+B,CAAA;IAE/B,8BAAa,CAAA;IAEb,8CAA6B,CAAA;AACjC,CAAC,EAnBW,aAAa,6BAAb,aAAa,QAmBxB","sourcesContent":["export enum FetchTypeEnum {\n AXIOS = 'axios',\n CF_BYPASS = 'cf_bypass',\n CF_BYPASS_EXTERNAL = 'cf_bypass_external',\n CF_BYPASS_EXTERNAL_AS_JSON = 'cf_bypass_external_as_json',\n PUPPETEER = 'puppeteer',\n PUPPETEER_AS_JSON = 'puppeteer_as_json',\n PUPPETEER_EXTERNAL_HOST = 'puppeteer_external_host',\n PUPPETEER_EXTERNAL_HOST_AS_JSON = 'puppeteer_external_host_as_json',\n\n EXTERNAL_HTTP_SERVICE = 'external_http_service',\n\n CURL_IMPERSONATE = 'curl_impersonate',\n\n CLOAK_BROWSER = 'cloak_browser',\n\n CURL = 'curl',\n\n FLARESOLVERR = 'flaresolverr',\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Product } from '../entities/product';
|
|
2
|
+
export type ChannelFilter = {
|
|
3
|
+
categories?: string[];
|
|
4
|
+
brands?: string[];
|
|
5
|
+
stores?: string[];
|
|
6
|
+
minDiscountPct?: number;
|
|
7
|
+
minPrice?: number;
|
|
8
|
+
maxPrice?: number;
|
|
9
|
+
words?: string[];
|
|
10
|
+
};
|
|
11
|
+
export declare function evalFilter(product: Product, filter: ChannelFilter): boolean;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.evalFilter = evalFilter;
|
|
4
|
+
const price_utils_1 = require("./price-utils");
|
|
5
|
+
function cleanSet(arr, lower) {
|
|
6
|
+
if (!Array.isArray(arr))
|
|
7
|
+
return null;
|
|
8
|
+
const out = [];
|
|
9
|
+
for (const v of arr) {
|
|
10
|
+
if (typeof v !== 'string')
|
|
11
|
+
continue;
|
|
12
|
+
const t = v.trim();
|
|
13
|
+
if (t.length === 0)
|
|
14
|
+
continue;
|
|
15
|
+
out.push(lower ? t.toLowerCase() : t);
|
|
16
|
+
}
|
|
17
|
+
return out.length > 0 ? out : null;
|
|
18
|
+
}
|
|
19
|
+
const finite = (n) => typeof n === 'number' && Number.isFinite(n);
|
|
20
|
+
function evalFilter(product, filter) {
|
|
21
|
+
var _a, _b, _c, _d;
|
|
22
|
+
const categories = cleanSet(filter.categories, true);
|
|
23
|
+
const brands = cleanSet(filter.brands, true);
|
|
24
|
+
const stores = cleanSet(filter.stores, false);
|
|
25
|
+
const words = cleanSet(filter.words, true);
|
|
26
|
+
const minDiscountPct = finite(filter.minDiscountPct) && filter.minDiscountPct > 0 ? filter.minDiscountPct : null;
|
|
27
|
+
const minPrice = finite(filter.minPrice) ? filter.minPrice : null;
|
|
28
|
+
const maxPrice = finite(filter.maxPrice) ? filter.maxPrice : null;
|
|
29
|
+
if (!categories && !brands && !stores && !words
|
|
30
|
+
&& minDiscountPct === null && minPrice === null && maxPrice === null) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (categories) {
|
|
34
|
+
const c = (_a = product.category) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
35
|
+
if (!c || !categories.includes(c))
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (brands) {
|
|
39
|
+
const b = (_b = product.brandName) === null || _b === void 0 ? void 0 : _b.toLowerCase();
|
|
40
|
+
if (!b || !brands.includes(b))
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (stores) {
|
|
44
|
+
if (!product.storeRef || !stores.includes(product.storeRef))
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (minDiscountPct !== null) {
|
|
48
|
+
const pct = (_c = product.priceStats) === null || _c === void 0 ? void 0 : _c.currentPricePrevPriceDiffPct;
|
|
49
|
+
if (pct === undefined || pct === null || pct < minDiscountPct)
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (minPrice !== null || maxPrice !== null) {
|
|
53
|
+
const currentPrice = (0, price_utils_1.getMinPrice)(product.price);
|
|
54
|
+
if (currentPrice === null || currentPrice === undefined)
|
|
55
|
+
return false;
|
|
56
|
+
if (minPrice !== null && currentPrice < minPrice)
|
|
57
|
+
return false;
|
|
58
|
+
if (maxPrice !== null && currentPrice > maxPrice)
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (words) {
|
|
62
|
+
const name = (_d = product.name) === null || _d === void 0 ? void 0 : _d.toLowerCase();
|
|
63
|
+
if (!name || !words.some((w) => name.includes(w)))
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=eval-filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eval-filter.js","sourceRoot":"/","sources":["utils/eval-filter.ts"],"names":[],"mappings":"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","sourcesContent":["import { Product } from '../entities/product';\nimport { getMinPrice } from './price-utils';\n\n/**\n * User-defined filter for an alert channel.\n * Every present field is AND-ed; an absent (or empty/blank) field is ignored.\n * A filter with NO effective fields returns false (safety floor).\n */\nexport type ChannelFilter = {\n /** Match if the product's category is any of these (case-insensitive). */\n categories?: string[];\n /** Match if the product's brandName is any of these (case-insensitive). */\n brands?: string[];\n /** Match if the product's storeRef is any of these (exact). */\n stores?: string[];\n /** Match if product.priceStats.currentPricePrevPriceDiffPct >= this (>0 to be active). */\n minDiscountPct?: number;\n /** Match if the product's current min price >= this value. */\n minPrice?: number;\n /** Match if the product's current min price <= this value. */\n maxPrice?: number;\n /** Match if any of these words appears (case-insensitive substring) in product.name. */\n words?: string[];\n};\n\n/** Trimmed, non-empty string elements (optionally lowercased), or null if none. */\nfunction cleanSet(arr: string[] | undefined, lower: boolean): string[] | null {\n if (!Array.isArray(arr)) return null;\n const out: string[] = [];\n for (const v of arr) {\n if (typeof v !== 'string') continue;\n const t = v.trim();\n if (t.length === 0) continue;\n out.push(lower ? t.toLowerCase() : t);\n }\n return out.length > 0 ? out : null;\n}\n\nconst finite = (n: number | undefined): n is number => typeof n === 'number' && Number.isFinite(n);\n\n/**\n * Pure, allocation-light predicate: true when the product matches every effective\n * field of the filter. Defensive against untrusted JSON filters (null/empty arrays,\n * null elements, blank words, non-numeric bounds are all ignored, never throw).\n *\n * Returns false when the filter has no effective fields (safety floor).\n *\n * NB: price bounds use getMinPrice(product.price), which (by existing lib behavior)\n * does not treat a validated offerPrice of 0 (isFree) as the min — free products are\n * not reliably caught by maxPrice:0.\n */\nexport function evalFilter(product: Product, filter: ChannelFilter): boolean {\n const categories = cleanSet(filter.categories, true);\n const brands = cleanSet(filter.brands, true);\n const stores = cleanSet(filter.stores, false); // storeRef is an id — exact, no lowercase\n const words = cleanSet(filter.words, true);\n // minDiscountPct of 0 (or negative/NaN) is \"no floor\" => inactive\n const minDiscountPct = finite(filter.minDiscountPct) && filter.minDiscountPct > 0 ? filter.minDiscountPct : null;\n const minPrice = finite(filter.minPrice) ? filter.minPrice : null;\n const maxPrice = finite(filter.maxPrice) ? filter.maxPrice : null;\n\n // Safety floor: a filter with no effective field must not match anything.\n if (!categories && !brands && !stores && !words\n && minDiscountPct === null && minPrice === null && maxPrice === null) {\n return false;\n }\n\n if (categories) {\n const c = product.category?.toLowerCase();\n if (!c || !categories.includes(c)) return false;\n }\n\n if (brands) {\n const b = product.brandName?.toLowerCase();\n if (!b || !brands.includes(b)) return false;\n }\n\n if (stores) {\n if (!product.storeRef || !stores.includes(product.storeRef)) return false;\n }\n\n if (minDiscountPct !== null) {\n const pct = product.priceStats?.currentPricePrevPriceDiffPct;\n if (pct === undefined || pct === null || pct < minDiscountPct) return false;\n }\n\n if (minPrice !== null || maxPrice !== null) {\n const currentPrice = getMinPrice(product.price);\n if (currentPrice === null || currentPrice === undefined) return false;\n if (minPrice !== null && currentPrice < minPrice) return false;\n if (maxPrice !== null && currentPrice > maxPrice) return false;\n }\n\n if (words) {\n const name = product.name?.toLowerCase();\n if (!name || !words.some((w) => name.includes(w))) return false;\n }\n\n return true;\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const product_1 = require("../entities/product");
|
|
4
|
+
const eval_filter_1 = require("./eval-filter");
|
|
5
|
+
function makeProduct(overrides = {}) {
|
|
6
|
+
const price = {
|
|
7
|
+
productRef: 'prod-1',
|
|
8
|
+
offerPrice: 50000,
|
|
9
|
+
normalPrice: 80000,
|
|
10
|
+
};
|
|
11
|
+
const priceStats = {
|
|
12
|
+
minPriceSum: 0,
|
|
13
|
+
maxPriceSum: 0,
|
|
14
|
+
avgPriceSum: 0,
|
|
15
|
+
pricesCount: 1,
|
|
16
|
+
avgMinPrice: 0,
|
|
17
|
+
avgAvgPrice: 0,
|
|
18
|
+
avgMaxPrice: 0,
|
|
19
|
+
currentMinPrice: 50000,
|
|
20
|
+
currentMaxPrice: 80000,
|
|
21
|
+
currentPriceAvgMinPriceDiffPct: 0,
|
|
22
|
+
currentPriceBestPriceDiffPct: 0,
|
|
23
|
+
currentPricePrevPriceDiffPct: 37.5,
|
|
24
|
+
currentMinPriceMaxPriceDiffPct: 37.5,
|
|
25
|
+
};
|
|
26
|
+
const base = {
|
|
27
|
+
name: 'Samsung Galaxy S24 Ultra 256GB',
|
|
28
|
+
storeRef: '5f27437dabd4b000086cd698',
|
|
29
|
+
brandName: 'Samsung',
|
|
30
|
+
category: 'Smartphones',
|
|
31
|
+
price,
|
|
32
|
+
priceStats,
|
|
33
|
+
};
|
|
34
|
+
return Object.assign(new product_1.Product(), base, overrides);
|
|
35
|
+
}
|
|
36
|
+
describe('evalFilter', () => {
|
|
37
|
+
it('returns false when filter has no fields set', () => {
|
|
38
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct(), {})).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
it('matches when product category is in filter.categories (exact, case-insensitive)', () => {
|
|
41
|
+
const filter = { categories: ['smartphones'] };
|
|
42
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
it('matches when product category is in filter.categories (mixed case)', () => {
|
|
45
|
+
const filter = { categories: ['SMARTPHONES', 'Laptops'] };
|
|
46
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
it('misses when product category is NOT in filter.categories', () => {
|
|
49
|
+
const filter = { categories: ['Laptops', 'Tablets'] };
|
|
50
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
it('misses when product has no category and filter.categories is set', () => {
|
|
53
|
+
const filter = { categories: ['Smartphones'] };
|
|
54
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct({ category: undefined }), filter)).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
it('matches when product brandName is in filter.brands (case-insensitive)', () => {
|
|
57
|
+
const filter = { brands: ['samsung'] };
|
|
58
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
it('misses when product brandName is NOT in filter.brands', () => {
|
|
61
|
+
const filter = { brands: ['Apple', 'LG'] };
|
|
62
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
it('matches when product storeRef is in filter.stores', () => {
|
|
65
|
+
const filter = { stores: ['5f27437dabd4b000086cd698'] };
|
|
66
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
it('misses when product storeRef is NOT in filter.stores', () => {
|
|
69
|
+
const filter = { stores: ['other-store-id'] };
|
|
70
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
it('matches when product discount equals minDiscountPct (boundary >=)', () => {
|
|
73
|
+
const filter = { minDiscountPct: 37.5 };
|
|
74
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
it('matches when product discount is above minDiscountPct', () => {
|
|
77
|
+
const filter = { minDiscountPct: 30 };
|
|
78
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
it('misses when product discount is below minDiscountPct', () => {
|
|
81
|
+
const filter = { minDiscountPct: 50 };
|
|
82
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
it('misses when product has no priceStats and minDiscountPct is set', () => {
|
|
85
|
+
const filter = { minDiscountPct: 10 };
|
|
86
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct({ priceStats: undefined }), filter)).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
it('matches when product min price equals minPrice (boundary >=)', () => {
|
|
89
|
+
const filter = { minPrice: 50000 };
|
|
90
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
it('misses when product min price is below minPrice', () => {
|
|
93
|
+
const filter = { minPrice: 60000 };
|
|
94
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
it('matches when product min price equals maxPrice (boundary <=)', () => {
|
|
97
|
+
const filter = { maxPrice: 50000 };
|
|
98
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
it('misses when product min price is above maxPrice', () => {
|
|
101
|
+
const filter = { maxPrice: 49999 };
|
|
102
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
it('matches when product price is within [minPrice, maxPrice] range', () => {
|
|
105
|
+
const filter = { minPrice: 40000, maxPrice: 60000 };
|
|
106
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
it('misses when product has no price and a price bound is set', () => {
|
|
109
|
+
const filter = { minPrice: 1000 };
|
|
110
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct({ price: undefined }), filter)).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
it('matches when any word appears (case-insensitive substring) in product name', () => {
|
|
113
|
+
const filter = { words: ['galaxy'] };
|
|
114
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
it('matches when second of two words appears in product name', () => {
|
|
117
|
+
const filter = { words: ['iphone', 'ultra'] };
|
|
118
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
it('misses when none of the words appear in product name', () => {
|
|
121
|
+
const filter = { words: ['iphone', 'pixel'] };
|
|
122
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
it('misses when product has no name and words filter is set', () => {
|
|
125
|
+
const filter = { words: ['galaxy'] };
|
|
126
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct({ name: undefined }), filter)).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
it('matches when all present fields pass', () => {
|
|
129
|
+
const filter = {
|
|
130
|
+
categories: ['smartphones'],
|
|
131
|
+
brands: ['samsung'],
|
|
132
|
+
stores: ['5f27437dabd4b000086cd698'],
|
|
133
|
+
minDiscountPct: 30,
|
|
134
|
+
minPrice: 40000,
|
|
135
|
+
maxPrice: 60000,
|
|
136
|
+
words: ['galaxy'],
|
|
137
|
+
};
|
|
138
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
it('misses when one of multiple present fields fails', () => {
|
|
141
|
+
const filter = {
|
|
142
|
+
categories: ['smartphones'],
|
|
143
|
+
brands: ['Apple'],
|
|
144
|
+
minDiscountPct: 30,
|
|
145
|
+
};
|
|
146
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
it('ignores absent optional fields (product with only storeRef filter)', () => {
|
|
149
|
+
const filter = { stores: ['5f27437dabd4b000086cd698'] };
|
|
150
|
+
expect((0, eval_filter_1.evalFilter)(makeProduct(), filter)).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
describe('evalFilter — robustness over untrusted JSON filters', () => {
|
|
154
|
+
const p = makeProduct();
|
|
155
|
+
it('ignores null/non-string array elements (no throw)', () => {
|
|
156
|
+
expect((0, eval_filter_1.evalFilter)(p, { brands: ['Samsung', null] })).toBe(true);
|
|
157
|
+
expect((0, eval_filter_1.evalFilter)(p, { categories: [null, 'Smartphones'] })).toBe(true);
|
|
158
|
+
expect((0, eval_filter_1.evalFilter)(p, { words: ['galaxy', null] })).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
it('treats a null field as absent (no throw)', () => {
|
|
161
|
+
expect((0, eval_filter_1.evalFilter)(p, { stores: null, brands: ['Samsung'] })).toBe(true);
|
|
162
|
+
expect((0, eval_filter_1.evalFilter)(p, { categories: null })).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
it('treats an empty array as absent, not match-nothing', () => {
|
|
165
|
+
expect((0, eval_filter_1.evalFilter)(p, { categories: [] })).toBe(false);
|
|
166
|
+
expect((0, eval_filter_1.evalFilter)(p, { categories: [], brands: ['Samsung'] })).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
it('ignores empty/whitespace word tokens (does not match everything)', () => {
|
|
169
|
+
expect((0, eval_filter_1.evalFilter)(p, { words: [''] })).toBe(false);
|
|
170
|
+
expect((0, eval_filter_1.evalFilter)(p, { words: [' '] })).toBe(false);
|
|
171
|
+
expect((0, eval_filter_1.evalFilter)(p, { words: [' galaxy '] })).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
it('minDiscountPct of 0 is no floor (inactive)', () => {
|
|
174
|
+
expect((0, eval_filter_1.evalFilter)(p, { minDiscountPct: 0 })).toBe(false);
|
|
175
|
+
expect((0, eval_filter_1.evalFilter)(p, { minDiscountPct: 0, brands: ['Samsung'] })).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
it('does not throw on a product missing priceStats/price/name/category/brand', () => {
|
|
178
|
+
const bare = makeProduct({
|
|
179
|
+
priceStats: undefined, price: undefined, name: undefined,
|
|
180
|
+
category: undefined, brandName: undefined, storeRef: undefined,
|
|
181
|
+
});
|
|
182
|
+
expect((0, eval_filter_1.evalFilter)(bare, { minDiscountPct: 30 })).toBe(false);
|
|
183
|
+
expect((0, eval_filter_1.evalFilter)(bare, { minPrice: 1 })).toBe(false);
|
|
184
|
+
expect((0, eval_filter_1.evalFilter)(bare, { words: ['x'] })).toBe(false);
|
|
185
|
+
expect(() => (0, eval_filter_1.evalFilter)(bare, {
|
|
186
|
+
brands: ['Samsung'], categories: ['x'], stores: ['y'], words: ['z'], minPrice: 1, maxPrice: 9, minDiscountPct: 5,
|
|
187
|
+
})).not.toThrow();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
//# sourceMappingURL=eval-filter.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eval-filter.spec.js","sourceRoot":"/","sources":["utils/eval-filter.spec.ts"],"names":[],"mappings":"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","sourcesContent":["import { Product } from '../entities/product';\nimport { Price } from '../entities/price';\nimport { PricesStats } from '../entities/prices-stats';\nimport { evalFilter, ChannelFilter } from './eval-filter';\n\n// ---------------------------------------------------------------------------\n// Minimal Product fixture (only fields used by evalFilter)\n// ---------------------------------------------------------------------------\nfunction makeProduct(overrides: Partial<Product> = {}): Product {\n const price = {\n productRef: 'prod-1',\n offerPrice: 50000,\n normalPrice: 80000,\n } as Price;\n\n const priceStats = {\n minPriceSum: 0,\n maxPriceSum: 0,\n avgPriceSum: 0,\n pricesCount: 1,\n avgMinPrice: 0,\n avgAvgPrice: 0,\n avgMaxPrice: 0,\n currentMinPrice: 50000,\n currentMaxPrice: 80000,\n currentPriceAvgMinPriceDiffPct: 0,\n currentPriceBestPriceDiffPct: 0,\n currentPricePrevPriceDiffPct: 37.5, // 37.5 % off vs previous price\n currentMinPriceMaxPriceDiffPct: 37.5,\n } as PricesStats;\n\n const base: Partial<Product> = {\n name: 'Samsung Galaxy S24 Ultra 256GB',\n storeRef: '5f27437dabd4b000086cd698', // Falabella store ref\n brandName: 'Samsung',\n category: 'Smartphones',\n price,\n priceStats,\n };\n\n return Object.assign(new Product(), base, overrides);\n}\n\n// ---------------------------------------------------------------------------\n// Tests\n// ---------------------------------------------------------------------------\n\ndescribe('evalFilter', () => {\n // --- empty filter guard ------------------------------------------------\n\n it('returns false when filter has no fields set', () => {\n expect(evalFilter(makeProduct(), {})).toBe(false);\n });\n\n // --- categories --------------------------------------------------------\n\n it('matches when product category is in filter.categories (exact, case-insensitive)', () => {\n const filter: ChannelFilter = { categories: ['smartphones'] };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('matches when product category is in filter.categories (mixed case)', () => {\n const filter: ChannelFilter = { categories: ['SMARTPHONES', 'Laptops'] };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when product category is NOT in filter.categories', () => {\n const filter: ChannelFilter = { categories: ['Laptops', 'Tablets'] };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n it('misses when product has no category and filter.categories is set', () => {\n const filter: ChannelFilter = { categories: ['Smartphones'] };\n expect(evalFilter(makeProduct({ category: undefined }), filter)).toBe(false);\n });\n\n // --- brands ------------------------------------------------------------\n\n it('matches when product brandName is in filter.brands (case-insensitive)', () => {\n const filter: ChannelFilter = { brands: ['samsung'] };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when product brandName is NOT in filter.brands', () => {\n const filter: ChannelFilter = { brands: ['Apple', 'LG'] };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n // --- stores ------------------------------------------------------------\n\n it('matches when product storeRef is in filter.stores', () => {\n const filter: ChannelFilter = { stores: ['5f27437dabd4b000086cd698'] };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when product storeRef is NOT in filter.stores', () => {\n const filter: ChannelFilter = { stores: ['other-store-id'] };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n // --- minDiscountPct ----------------------------------------------------\n\n it('matches when product discount equals minDiscountPct (boundary >=)', () => {\n // currentPricePrevPriceDiffPct is 37.5 in fixture\n const filter: ChannelFilter = { minDiscountPct: 37.5 };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('matches when product discount is above minDiscountPct', () => {\n const filter: ChannelFilter = { minDiscountPct: 30 };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when product discount is below minDiscountPct', () => {\n const filter: ChannelFilter = { minDiscountPct: 50 };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n it('misses when product has no priceStats and minDiscountPct is set', () => {\n const filter: ChannelFilter = { minDiscountPct: 10 };\n expect(evalFilter(makeProduct({ priceStats: undefined }), filter)).toBe(false);\n });\n\n // --- minPrice / maxPrice -----------------------------------------------\n\n it('matches when product min price equals minPrice (boundary >=)', () => {\n // getMinPrice(price) = min(50000, 80000) = 50000\n const filter: ChannelFilter = { minPrice: 50000 };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when product min price is below minPrice', () => {\n const filter: ChannelFilter = { minPrice: 60000 };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n it('matches when product min price equals maxPrice (boundary <=)', () => {\n const filter: ChannelFilter = { maxPrice: 50000 };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when product min price is above maxPrice', () => {\n const filter: ChannelFilter = { maxPrice: 49999 };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n it('matches when product price is within [minPrice, maxPrice] range', () => {\n const filter: ChannelFilter = { minPrice: 40000, maxPrice: 60000 };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when product has no price and a price bound is set', () => {\n const filter: ChannelFilter = { minPrice: 1000 };\n expect(evalFilter(makeProduct({ price: undefined }), filter)).toBe(false);\n });\n\n // --- words -------------------------------------------------------------\n\n it('matches when any word appears (case-insensitive substring) in product name', () => {\n const filter: ChannelFilter = { words: ['galaxy'] };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('matches when second of two words appears in product name', () => {\n const filter: ChannelFilter = { words: ['iphone', 'ultra'] };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when none of the words appear in product name', () => {\n const filter: ChannelFilter = { words: ['iphone', 'pixel'] };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n it('misses when product has no name and words filter is set', () => {\n const filter: ChannelFilter = { words: ['galaxy'] };\n expect(evalFilter(makeProduct({ name: undefined }), filter)).toBe(false);\n });\n\n // --- combined fields (AND logic) ---------------------------------------\n\n it('matches when all present fields pass', () => {\n const filter: ChannelFilter = {\n categories: ['smartphones'],\n brands: ['samsung'],\n stores: ['5f27437dabd4b000086cd698'],\n minDiscountPct: 30,\n minPrice: 40000,\n maxPrice: 60000,\n words: ['galaxy'],\n };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n\n it('misses when one of multiple present fields fails', () => {\n const filter: ChannelFilter = {\n categories: ['smartphones'],\n brands: ['Apple'], // <-- this will fail\n minDiscountPct: 30,\n };\n expect(evalFilter(makeProduct(), filter)).toBe(false);\n });\n\n it('ignores absent optional fields (product with only storeRef filter)', () => {\n const filter: ChannelFilter = { stores: ['5f27437dabd4b000086cd698'] };\n expect(evalFilter(makeProduct(), filter)).toBe(true);\n });\n});\n\ndescribe('evalFilter — robustness over untrusted JSON filters', () => {\n const p = makeProduct(); // Samsung / Smartphones / store ref / \"Galaxy\" / 37.5% off\n\n it('ignores null/non-string array elements (no throw)', () => {\n expect(evalFilter(p, { brands: ['Samsung', null as never] })).toBe(true);\n expect(evalFilter(p, { categories: [null as never, 'Smartphones'] })).toBe(true);\n expect(evalFilter(p, { words: ['galaxy', null as never] })).toBe(true);\n });\n\n it('treats a null field as absent (no throw)', () => {\n expect(evalFilter(p, { stores: null as never, brands: ['Samsung'] })).toBe(true);\n expect(evalFilter(p, { categories: null as never })).toBe(false); // no effective field\n });\n\n it('treats an empty array as absent, not match-nothing', () => {\n expect(evalFilter(p, { categories: [] })).toBe(false); // only field, empty => floor\n expect(evalFilter(p, { categories: [], brands: ['Samsung'] })).toBe(true); // empty ignored, brand matches\n });\n\n it('ignores empty/whitespace word tokens (does not match everything)', () => {\n expect(evalFilter(p, { words: [''] })).toBe(false);\n expect(evalFilter(p, { words: [' '] })).toBe(false);\n expect(evalFilter(p, { words: [' galaxy '] })).toBe(true); // trimmed\n });\n\n it('minDiscountPct of 0 is no floor (inactive)', () => {\n expect(evalFilter(p, { minDiscountPct: 0 })).toBe(false); // only field => floor\n expect(evalFilter(p, { minDiscountPct: 0, brands: ['Samsung'] })).toBe(true);\n });\n\n it('does not throw on a product missing priceStats/price/name/category/brand', () => {\n const bare = makeProduct({\n priceStats: undefined as never, price: undefined as never, name: undefined as never,\n category: undefined as never, brandName: undefined as never, storeRef: undefined as never,\n });\n expect(evalFilter(bare, { minDiscountPct: 30 })).toBe(false);\n expect(evalFilter(bare, { minPrice: 1 })).toBe(false);\n expect(evalFilter(bare, { words: ['x'] })).toBe(false);\n expect(() => evalFilter(bare, {\n brands: ['Samsung'], categories: ['x'], stores: ['y'], words: ['z'], minPrice: 1, maxPrice: 9, minDiscountPct: 5,\n })).not.toThrow();\n });\n});\n"]}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -6,4 +6,6 @@ import { calculateAverage, roundNumber, getRandomNumber, getRandomNumbers, getPe
|
|
|
6
6
|
import { hasElementInArray, isArrayEmpty, stringToArray, getRandomItem } from './array-utils';
|
|
7
7
|
import { hasUpdateReason, getStoreName, parseProductDtoToTypesenseDocument } from './product.utils';
|
|
8
8
|
import { formatDate, formatMoney, formatPct } from './string-formatter';
|
|
9
|
-
|
|
9
|
+
import { evalFilter, ChannelFilter } from './eval-filter';
|
|
10
|
+
export { checkIfSamePrices, findLastQuery, replaceMultipleSpacesByOne, hasMeasurementUnitMultiplier, getPriceByMeasurementUnitMultiplier, removeIfEndsWith, getLastAfter, includesStringArray, removeNewLines, reverseString, getParam, getPathname, getHostname, isUrl, isValidPrice, getMinPrice, isCheaper, getMaxPrice, getPriceOperation, getAvgPrice, calculateAverage, roundNumber, removeZeroPrices, isArrayEmpty, hasElementInArray, stringToArray, hasUpdateReason, getStoreName, parseProductDtoToTypesenseDocument, formatPct, formatDate, formatMoney, getRandomNumber, getRandomNumbers, getPercentageDiff, getRandomItem, evalFilter, };
|
|
11
|
+
export type { ChannelFilter };
|
package/dist/utils/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getRandomItem = exports.getPercentageDiff = exports.getRandomNumbers = exports.getRandomNumber = exports.formatMoney = exports.formatDate = exports.formatPct = exports.parseProductDtoToTypesenseDocument = exports.getStoreName = exports.hasUpdateReason = exports.stringToArray = exports.hasElementInArray = exports.isArrayEmpty = exports.removeZeroPrices = exports.roundNumber = exports.calculateAverage = exports.getAvgPrice = exports.getPriceOperation = exports.getMaxPrice = exports.isCheaper = exports.getMinPrice = exports.isValidPrice = exports.isUrl = exports.getHostname = exports.getPathname = exports.getParam = exports.reverseString = exports.removeNewLines = exports.includesStringArray = exports.getLastAfter = exports.removeIfEndsWith = exports.getPriceByMeasurementUnitMultiplier = exports.hasMeasurementUnitMultiplier = exports.replaceMultipleSpacesByOne = exports.findLastQuery = exports.checkIfSamePrices = void 0;
|
|
3
|
+
exports.evalFilter = exports.getRandomItem = exports.getPercentageDiff = exports.getRandomNumbers = exports.getRandomNumber = exports.formatMoney = exports.formatDate = exports.formatPct = exports.parseProductDtoToTypesenseDocument = exports.getStoreName = exports.hasUpdateReason = exports.stringToArray = exports.hasElementInArray = exports.isArrayEmpty = exports.removeZeroPrices = exports.roundNumber = exports.calculateAverage = exports.getAvgPrice = exports.getPriceOperation = exports.getMaxPrice = exports.isCheaper = exports.getMinPrice = exports.isValidPrice = exports.isUrl = exports.getHostname = exports.getPathname = exports.getParam = exports.reverseString = exports.removeNewLines = exports.includesStringArray = exports.getLastAfter = exports.removeIfEndsWith = exports.getPriceByMeasurementUnitMultiplier = exports.hasMeasurementUnitMultiplier = exports.replaceMultipleSpacesByOne = exports.findLastQuery = exports.checkIfSamePrices = void 0;
|
|
4
4
|
const price_utils_1 = require("./price-utils");
|
|
5
5
|
Object.defineProperty(exports, "checkIfSamePrices", { enumerable: true, get: function () { return price_utils_1.checkIfSamePrices; } });
|
|
6
6
|
Object.defineProperty(exports, "isCheaper", { enumerable: true, get: function () { return price_utils_1.isCheaper; } });
|
|
@@ -45,4 +45,6 @@ const string_formatter_1 = require("./string-formatter");
|
|
|
45
45
|
Object.defineProperty(exports, "formatDate", { enumerable: true, get: function () { return string_formatter_1.formatDate; } });
|
|
46
46
|
Object.defineProperty(exports, "formatMoney", { enumerable: true, get: function () { return string_formatter_1.formatMoney; } });
|
|
47
47
|
Object.defineProperty(exports, "formatPct", { enumerable: true, get: function () { return string_formatter_1.formatPct; } });
|
|
48
|
+
const eval_filter_1 = require("./eval-filter");
|
|
49
|
+
Object.defineProperty(exports, "evalFilter", { enumerable: true, get: function () { return eval_filter_1.evalFilter; } });
|
|
48
50
|
//# sourceMappingURL=index.js.map
|
package/dist/utils/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"/","sources":["utils/index.ts"],"names":[],"mappings":";;;AAAA,+CAGuB;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"/","sources":["utils/index.ts"],"names":[],"mappings":";;;AAAA,+CAGuB;AAoBrB,kGAtBA,+BAAiB,OAsBA;AAE8B,0FAxB5B,uBAAS,OAwB4B;AAAtB,4FAxBJ,yBAAW,OAwBI;AAAzB,6FAxBuB,0BAAY,OAwBvB;AAA0D,4FAxBjC,yBAAW,OAwBiC;AAA3C,4FAxBY,yBAAW,OAwBZ;AAAE,kGAxBY,+BAAiB,OAwBZ;AAC3E,iGAzByF,8BAAgB,OAyBzF;AAHiC,6GArB9D,0CAA4B,OAqB8D;AAAE,oHArB9D,iDAAmC,OAqB8D;AAnBjI,iEAAuD;AAmBlC,8FAnBZ,oCAAa,OAmBY;AAlBlC,iDAGwB;AAgB+C,8FAlBrE,4BAAa,OAkBqE;AAA7B,+FAlBtC,6BAAc,OAkBsC;AAAnC,oGAlBD,kCAAmB,OAkBC;AAAjC,6FAlBkC,2BAAY,OAkBlC;AADI,2GAjBgC,yCAA0B,OAiBhC;AAC5D,iGAlB8F,+BAAgB,OAkB9F;AAflB,2CAEqB;AAcN,sFAfb,iBAAK,OAea;AAAlB,4FAfO,uBAAW,OAeP;AADyE,yFAdhE,oBAAQ,OAcgE;AAAE,4FAdhE,uBAAW,OAcgE;AAZ3G,iDAEwB;AAWiF,iGAZvG,+BAAgB,OAYuG;AACvH,4FAbkB,0BAAW,OAalB;AACyB,gGAdL,8BAAe,OAcK;AAAE,iGAdL,+BAAgB,OAcK;AAAE,kGAdL,gCAAiB,OAcK;AAZ1F,+CAEuB;AASwB,kGAV7C,+BAAiB,OAU6C;AAA/B,6FAVZ,0BAAY,OAUY;AAAqB,8FAV/B,2BAAa,OAU+B;AACa,8FAX1C,2BAAa,OAW0C;AATzG,mDAAoG;AAQnB,gGARxE,+BAAe,OAQwE;AAAE,6FARxE,4BAAY,OAQwE;AAAE,mHARxE,kDAAkC,OAQwE;AAPlJ,yDAAwE;AAQ3D,2FARJ,6BAAU,OAQI;AAAE,4FARJ,8BAAW,OAQI;AAAlC,0FARgC,4BAAS,OAQhC;AAPX,+CAA0D;AAQxD,2FARO,wBAAU,OAQP","sourcesContent":["import {\n checkIfSamePrices, isCheaper, getMinPrice, isValidPrice, getAvgPrice, getMaxPrice, getPriceOperation, removeZeroPrices,\n hasMeasurementUnitMultiplier, getPriceByMeasurementUnitMultiplier,\n} from './price-utils';\nimport { findLastQuery } from './entity-queries-utils';\nimport {\n reverseString, removeNewLines, includesStringArray, getLastAfter, replaceMultipleSpacesByOne, removeIfEndsWith,\n}\n from './string-utils';\nimport {\n isUrl, getHostname, getParam, getPathname,\n} from './url-utils';\nimport {\n calculateAverage, roundNumber, getRandomNumber, getRandomNumbers, getPercentageDiff,\n} from './number-utils';\nimport {\n hasElementInArray, isArrayEmpty, stringToArray, getRandomItem,\n} from './array-utils';\nimport { hasUpdateReason, getStoreName, parseProductDtoToTypesenseDocument } from './product.utils';\nimport { formatDate, formatMoney, formatPct } from './string-formatter';\nimport { evalFilter, ChannelFilter } from './eval-filter';\n\nexport {\n checkIfSamePrices, findLastQuery, replaceMultipleSpacesByOne, hasMeasurementUnitMultiplier, getPriceByMeasurementUnitMultiplier,\n removeIfEndsWith, getLastAfter, includesStringArray, removeNewLines, reverseString, getParam, getPathname,\n getHostname, isUrl, isValidPrice, getMinPrice, isCheaper, getMaxPrice, getPriceOperation, getAvgPrice, calculateAverage,\n roundNumber, removeZeroPrices, isArrayEmpty, hasElementInArray, stringToArray, hasUpdateReason, getStoreName, parseProductDtoToTypesenseDocument,\n formatPct, formatDate, formatMoney, getRandomNumber, getRandomNumbers, getPercentageDiff, getRandomItem,\n evalFilter,\n};\n\nexport type { ChannelFilter };\n"]}
|
package/package.json
CHANGED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { Product } from '../entities/product';
|
|
2
|
+
import { Price } from '../entities/price';
|
|
3
|
+
import { PricesStats } from '../entities/prices-stats';
|
|
4
|
+
import { evalFilter, ChannelFilter } from './eval-filter';
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Minimal Product fixture (only fields used by evalFilter)
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
function makeProduct(overrides: Partial<Product> = {}): Product {
|
|
10
|
+
const price = {
|
|
11
|
+
productRef: 'prod-1',
|
|
12
|
+
offerPrice: 50000,
|
|
13
|
+
normalPrice: 80000,
|
|
14
|
+
} as Price;
|
|
15
|
+
|
|
16
|
+
const priceStats = {
|
|
17
|
+
minPriceSum: 0,
|
|
18
|
+
maxPriceSum: 0,
|
|
19
|
+
avgPriceSum: 0,
|
|
20
|
+
pricesCount: 1,
|
|
21
|
+
avgMinPrice: 0,
|
|
22
|
+
avgAvgPrice: 0,
|
|
23
|
+
avgMaxPrice: 0,
|
|
24
|
+
currentMinPrice: 50000,
|
|
25
|
+
currentMaxPrice: 80000,
|
|
26
|
+
currentPriceAvgMinPriceDiffPct: 0,
|
|
27
|
+
currentPriceBestPriceDiffPct: 0,
|
|
28
|
+
currentPricePrevPriceDiffPct: 37.5, // 37.5 % off vs previous price
|
|
29
|
+
currentMinPriceMaxPriceDiffPct: 37.5,
|
|
30
|
+
} as PricesStats;
|
|
31
|
+
|
|
32
|
+
const base: Partial<Product> = {
|
|
33
|
+
name: 'Samsung Galaxy S24 Ultra 256GB',
|
|
34
|
+
storeRef: '5f27437dabd4b000086cd698', // Falabella store ref
|
|
35
|
+
brandName: 'Samsung',
|
|
36
|
+
category: 'Smartphones',
|
|
37
|
+
price,
|
|
38
|
+
priceStats,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return Object.assign(new Product(), base, overrides);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Tests
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
describe('evalFilter', () => {
|
|
49
|
+
// --- empty filter guard ------------------------------------------------
|
|
50
|
+
|
|
51
|
+
it('returns false when filter has no fields set', () => {
|
|
52
|
+
expect(evalFilter(makeProduct(), {})).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// --- categories --------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
it('matches when product category is in filter.categories (exact, case-insensitive)', () => {
|
|
58
|
+
const filter: ChannelFilter = { categories: ['smartphones'] };
|
|
59
|
+
expect(evalFilter(makeProduct(), filter)).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('matches when product category is in filter.categories (mixed case)', () => {
|
|
63
|
+
const filter: ChannelFilter = { categories: ['SMARTPHONES', 'Laptops'] };
|
|
64
|
+
expect(evalFilter(makeProduct(), filter)).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('misses when product category is NOT in filter.categories', () => {
|
|
68
|
+
const filter: ChannelFilter = { categories: ['Laptops', 'Tablets'] };
|
|
69
|
+
expect(evalFilter(makeProduct(), filter)).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('misses when product has no category and filter.categories is set', () => {
|
|
73
|
+
const filter: ChannelFilter = { categories: ['Smartphones'] };
|
|
74
|
+
expect(evalFilter(makeProduct({ category: undefined }), filter)).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// --- brands ------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
it('matches when product brandName is in filter.brands (case-insensitive)', () => {
|
|
80
|
+
const filter: ChannelFilter = { brands: ['samsung'] };
|
|
81
|
+
expect(evalFilter(makeProduct(), filter)).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('misses when product brandName is NOT in filter.brands', () => {
|
|
85
|
+
const filter: ChannelFilter = { brands: ['Apple', 'LG'] };
|
|
86
|
+
expect(evalFilter(makeProduct(), filter)).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// --- stores ------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
it('matches when product storeRef is in filter.stores', () => {
|
|
92
|
+
const filter: ChannelFilter = { stores: ['5f27437dabd4b000086cd698'] };
|
|
93
|
+
expect(evalFilter(makeProduct(), filter)).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('misses when product storeRef is NOT in filter.stores', () => {
|
|
97
|
+
const filter: ChannelFilter = { stores: ['other-store-id'] };
|
|
98
|
+
expect(evalFilter(makeProduct(), filter)).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// --- minDiscountPct ----------------------------------------------------
|
|
102
|
+
|
|
103
|
+
it('matches when product discount equals minDiscountPct (boundary >=)', () => {
|
|
104
|
+
// currentPricePrevPriceDiffPct is 37.5 in fixture
|
|
105
|
+
const filter: ChannelFilter = { minDiscountPct: 37.5 };
|
|
106
|
+
expect(evalFilter(makeProduct(), filter)).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('matches when product discount is above minDiscountPct', () => {
|
|
110
|
+
const filter: ChannelFilter = { minDiscountPct: 30 };
|
|
111
|
+
expect(evalFilter(makeProduct(), filter)).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('misses when product discount is below minDiscountPct', () => {
|
|
115
|
+
const filter: ChannelFilter = { minDiscountPct: 50 };
|
|
116
|
+
expect(evalFilter(makeProduct(), filter)).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('misses when product has no priceStats and minDiscountPct is set', () => {
|
|
120
|
+
const filter: ChannelFilter = { minDiscountPct: 10 };
|
|
121
|
+
expect(evalFilter(makeProduct({ priceStats: undefined }), filter)).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// --- minPrice / maxPrice -----------------------------------------------
|
|
125
|
+
|
|
126
|
+
it('matches when product min price equals minPrice (boundary >=)', () => {
|
|
127
|
+
// getMinPrice(price) = min(50000, 80000) = 50000
|
|
128
|
+
const filter: ChannelFilter = { minPrice: 50000 };
|
|
129
|
+
expect(evalFilter(makeProduct(), filter)).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('misses when product min price is below minPrice', () => {
|
|
133
|
+
const filter: ChannelFilter = { minPrice: 60000 };
|
|
134
|
+
expect(evalFilter(makeProduct(), filter)).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('matches when product min price equals maxPrice (boundary <=)', () => {
|
|
138
|
+
const filter: ChannelFilter = { maxPrice: 50000 };
|
|
139
|
+
expect(evalFilter(makeProduct(), filter)).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('misses when product min price is above maxPrice', () => {
|
|
143
|
+
const filter: ChannelFilter = { maxPrice: 49999 };
|
|
144
|
+
expect(evalFilter(makeProduct(), filter)).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('matches when product price is within [minPrice, maxPrice] range', () => {
|
|
148
|
+
const filter: ChannelFilter = { minPrice: 40000, maxPrice: 60000 };
|
|
149
|
+
expect(evalFilter(makeProduct(), filter)).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('misses when product has no price and a price bound is set', () => {
|
|
153
|
+
const filter: ChannelFilter = { minPrice: 1000 };
|
|
154
|
+
expect(evalFilter(makeProduct({ price: undefined }), filter)).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// --- words -------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
it('matches when any word appears (case-insensitive substring) in product name', () => {
|
|
160
|
+
const filter: ChannelFilter = { words: ['galaxy'] };
|
|
161
|
+
expect(evalFilter(makeProduct(), filter)).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('matches when second of two words appears in product name', () => {
|
|
165
|
+
const filter: ChannelFilter = { words: ['iphone', 'ultra'] };
|
|
166
|
+
expect(evalFilter(makeProduct(), filter)).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('misses when none of the words appear in product name', () => {
|
|
170
|
+
const filter: ChannelFilter = { words: ['iphone', 'pixel'] };
|
|
171
|
+
expect(evalFilter(makeProduct(), filter)).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('misses when product has no name and words filter is set', () => {
|
|
175
|
+
const filter: ChannelFilter = { words: ['galaxy'] };
|
|
176
|
+
expect(evalFilter(makeProduct({ name: undefined }), filter)).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// --- combined fields (AND logic) ---------------------------------------
|
|
180
|
+
|
|
181
|
+
it('matches when all present fields pass', () => {
|
|
182
|
+
const filter: ChannelFilter = {
|
|
183
|
+
categories: ['smartphones'],
|
|
184
|
+
brands: ['samsung'],
|
|
185
|
+
stores: ['5f27437dabd4b000086cd698'],
|
|
186
|
+
minDiscountPct: 30,
|
|
187
|
+
minPrice: 40000,
|
|
188
|
+
maxPrice: 60000,
|
|
189
|
+
words: ['galaxy'],
|
|
190
|
+
};
|
|
191
|
+
expect(evalFilter(makeProduct(), filter)).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('misses when one of multiple present fields fails', () => {
|
|
195
|
+
const filter: ChannelFilter = {
|
|
196
|
+
categories: ['smartphones'],
|
|
197
|
+
brands: ['Apple'], // <-- this will fail
|
|
198
|
+
minDiscountPct: 30,
|
|
199
|
+
};
|
|
200
|
+
expect(evalFilter(makeProduct(), filter)).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('ignores absent optional fields (product with only storeRef filter)', () => {
|
|
204
|
+
const filter: ChannelFilter = { stores: ['5f27437dabd4b000086cd698'] };
|
|
205
|
+
expect(evalFilter(makeProduct(), filter)).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('evalFilter — robustness over untrusted JSON filters', () => {
|
|
210
|
+
const p = makeProduct(); // Samsung / Smartphones / store ref / "Galaxy" / 37.5% off
|
|
211
|
+
|
|
212
|
+
it('ignores null/non-string array elements (no throw)', () => {
|
|
213
|
+
expect(evalFilter(p, { brands: ['Samsung', null as never] })).toBe(true);
|
|
214
|
+
expect(evalFilter(p, { categories: [null as never, 'Smartphones'] })).toBe(true);
|
|
215
|
+
expect(evalFilter(p, { words: ['galaxy', null as never] })).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('treats a null field as absent (no throw)', () => {
|
|
219
|
+
expect(evalFilter(p, { stores: null as never, brands: ['Samsung'] })).toBe(true);
|
|
220
|
+
expect(evalFilter(p, { categories: null as never })).toBe(false); // no effective field
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('treats an empty array as absent, not match-nothing', () => {
|
|
224
|
+
expect(evalFilter(p, { categories: [] })).toBe(false); // only field, empty => floor
|
|
225
|
+
expect(evalFilter(p, { categories: [], brands: ['Samsung'] })).toBe(true); // empty ignored, brand matches
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('ignores empty/whitespace word tokens (does not match everything)', () => {
|
|
229
|
+
expect(evalFilter(p, { words: [''] })).toBe(false);
|
|
230
|
+
expect(evalFilter(p, { words: [' '] })).toBe(false);
|
|
231
|
+
expect(evalFilter(p, { words: [' galaxy '] })).toBe(true); // trimmed
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('minDiscountPct of 0 is no floor (inactive)', () => {
|
|
235
|
+
expect(evalFilter(p, { minDiscountPct: 0 })).toBe(false); // only field => floor
|
|
236
|
+
expect(evalFilter(p, { minDiscountPct: 0, brands: ['Samsung'] })).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('does not throw on a product missing priceStats/price/name/category/brand', () => {
|
|
240
|
+
const bare = makeProduct({
|
|
241
|
+
priceStats: undefined as never, price: undefined as never, name: undefined as never,
|
|
242
|
+
category: undefined as never, brandName: undefined as never, storeRef: undefined as never,
|
|
243
|
+
});
|
|
244
|
+
expect(evalFilter(bare, { minDiscountPct: 30 })).toBe(false);
|
|
245
|
+
expect(evalFilter(bare, { minPrice: 1 })).toBe(false);
|
|
246
|
+
expect(evalFilter(bare, { words: ['x'] })).toBe(false);
|
|
247
|
+
expect(() => evalFilter(bare, {
|
|
248
|
+
brands: ['Samsung'], categories: ['x'], stores: ['y'], words: ['z'], minPrice: 1, maxPrice: 9, minDiscountPct: 5,
|
|
249
|
+
})).not.toThrow();
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Product } from '../entities/product';
|
|
2
|
+
import { getMinPrice } from './price-utils';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* User-defined filter for an alert channel.
|
|
6
|
+
* Every present field is AND-ed; an absent (or empty/blank) field is ignored.
|
|
7
|
+
* A filter with NO effective fields returns false (safety floor).
|
|
8
|
+
*/
|
|
9
|
+
export type ChannelFilter = {
|
|
10
|
+
/** Match if the product's category is any of these (case-insensitive). */
|
|
11
|
+
categories?: string[];
|
|
12
|
+
/** Match if the product's brandName is any of these (case-insensitive). */
|
|
13
|
+
brands?: string[];
|
|
14
|
+
/** Match if the product's storeRef is any of these (exact). */
|
|
15
|
+
stores?: string[];
|
|
16
|
+
/** Match if product.priceStats.currentPricePrevPriceDiffPct >= this (>0 to be active). */
|
|
17
|
+
minDiscountPct?: number;
|
|
18
|
+
/** Match if the product's current min price >= this value. */
|
|
19
|
+
minPrice?: number;
|
|
20
|
+
/** Match if the product's current min price <= this value. */
|
|
21
|
+
maxPrice?: number;
|
|
22
|
+
/** Match if any of these words appears (case-insensitive substring) in product.name. */
|
|
23
|
+
words?: string[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Trimmed, non-empty string elements (optionally lowercased), or null if none. */
|
|
27
|
+
function cleanSet(arr: string[] | undefined, lower: boolean): string[] | null {
|
|
28
|
+
if (!Array.isArray(arr)) return null;
|
|
29
|
+
const out: string[] = [];
|
|
30
|
+
for (const v of arr) {
|
|
31
|
+
if (typeof v !== 'string') continue;
|
|
32
|
+
const t = v.trim();
|
|
33
|
+
if (t.length === 0) continue;
|
|
34
|
+
out.push(lower ? t.toLowerCase() : t);
|
|
35
|
+
}
|
|
36
|
+
return out.length > 0 ? out : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const finite = (n: number | undefined): n is number => typeof n === 'number' && Number.isFinite(n);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Pure, allocation-light predicate: true when the product matches every effective
|
|
43
|
+
* field of the filter. Defensive against untrusted JSON filters (null/empty arrays,
|
|
44
|
+
* null elements, blank words, non-numeric bounds are all ignored, never throw).
|
|
45
|
+
*
|
|
46
|
+
* Returns false when the filter has no effective fields (safety floor).
|
|
47
|
+
*
|
|
48
|
+
* NB: price bounds use getMinPrice(product.price), which (by existing lib behavior)
|
|
49
|
+
* does not treat a validated offerPrice of 0 (isFree) as the min — free products are
|
|
50
|
+
* not reliably caught by maxPrice:0.
|
|
51
|
+
*/
|
|
52
|
+
export function evalFilter(product: Product, filter: ChannelFilter): boolean {
|
|
53
|
+
const categories = cleanSet(filter.categories, true);
|
|
54
|
+
const brands = cleanSet(filter.brands, true);
|
|
55
|
+
const stores = cleanSet(filter.stores, false); // storeRef is an id — exact, no lowercase
|
|
56
|
+
const words = cleanSet(filter.words, true);
|
|
57
|
+
// minDiscountPct of 0 (or negative/NaN) is "no floor" => inactive
|
|
58
|
+
const minDiscountPct = finite(filter.minDiscountPct) && filter.minDiscountPct > 0 ? filter.minDiscountPct : null;
|
|
59
|
+
const minPrice = finite(filter.minPrice) ? filter.minPrice : null;
|
|
60
|
+
const maxPrice = finite(filter.maxPrice) ? filter.maxPrice : null;
|
|
61
|
+
|
|
62
|
+
// Safety floor: a filter with no effective field must not match anything.
|
|
63
|
+
if (!categories && !brands && !stores && !words
|
|
64
|
+
&& minDiscountPct === null && minPrice === null && maxPrice === null) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (categories) {
|
|
69
|
+
const c = product.category?.toLowerCase();
|
|
70
|
+
if (!c || !categories.includes(c)) return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (brands) {
|
|
74
|
+
const b = product.brandName?.toLowerCase();
|
|
75
|
+
if (!b || !brands.includes(b)) return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (stores) {
|
|
79
|
+
if (!product.storeRef || !stores.includes(product.storeRef)) return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (minDiscountPct !== null) {
|
|
83
|
+
const pct = product.priceStats?.currentPricePrevPriceDiffPct;
|
|
84
|
+
if (pct === undefined || pct === null || pct < minDiscountPct) return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (minPrice !== null || maxPrice !== null) {
|
|
88
|
+
const currentPrice = getMinPrice(product.price);
|
|
89
|
+
if (currentPrice === null || currentPrice === undefined) return false;
|
|
90
|
+
if (minPrice !== null && currentPrice < minPrice) return false;
|
|
91
|
+
if (maxPrice !== null && currentPrice > maxPrice) return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (words) {
|
|
95
|
+
const name = product.name?.toLowerCase();
|
|
96
|
+
if (!name || !words.some((w) => name.includes(w))) return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return true;
|
|
100
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from './array-utils';
|
|
19
19
|
import { hasUpdateReason, getStoreName, parseProductDtoToTypesenseDocument } from './product.utils';
|
|
20
20
|
import { formatDate, formatMoney, formatPct } from './string-formatter';
|
|
21
|
+
import { evalFilter, ChannelFilter } from './eval-filter';
|
|
21
22
|
|
|
22
23
|
export {
|
|
23
24
|
checkIfSamePrices, findLastQuery, replaceMultipleSpacesByOne, hasMeasurementUnitMultiplier, getPriceByMeasurementUnitMultiplier,
|
|
@@ -25,4 +26,7 @@ export {
|
|
|
25
26
|
getHostname, isUrl, isValidPrice, getMinPrice, isCheaper, getMaxPrice, getPriceOperation, getAvgPrice, calculateAverage,
|
|
26
27
|
roundNumber, removeZeroPrices, isArrayEmpty, hasElementInArray, stringToArray, hasUpdateReason, getStoreName, parseProductDtoToTypesenseDocument,
|
|
27
28
|
formatPct, formatDate, formatMoney, getRandomNumber, getRandomNumbers, getPercentageDiff, getRandomItem,
|
|
29
|
+
evalFilter,
|
|
28
30
|
};
|
|
31
|
+
|
|
32
|
+
export type { ChannelFilter };
|