Skip to content

Commit 4fe93e7

Browse files
committed
Bump 2.4.1 and harden pagination inputs
1 parent 9cee05d commit 4fe93e7

File tree

6 files changed

+155
-11
lines changed

6 files changed

+155
-11
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "itemsjs",
3-
"version": "2.4.0",
3+
"version": "2.4.1",
44
"description": "Created to perform fast search on small json dataset (up to 1000 elements).",
55
"type": "module",
66
"scripts": {

src/lib.js

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,28 @@ import { getBuckets, clone } from './helpers.js';
88
export function search(items, input, configuration, fulltext, facets) {
99
input = input || Object.create(null);
1010

11-
const per_page = parseInt(input.per_page || 12);
12-
const page = parseInt(input.page || 1);
11+
const normalizeNumber = (value) => {
12+
if (typeof value === 'number') {
13+
return value;
14+
}
15+
const parsed = parseInt(value, 10);
16+
return parsed;
17+
};
18+
19+
let per_page = normalizeNumber(input.per_page);
20+
if (!Number.isFinite(per_page) || per_page < 0) {
21+
per_page = 12;
22+
}
23+
24+
let page = normalizeNumber(input.page);
25+
if (!Number.isFinite(page) || page < 1) {
26+
page = 1;
27+
}
28+
29+
// Allow per_page to be zero to support queries that only need aggregations
30+
if (per_page === 0) {
31+
page = 1;
32+
}
1333
const is_all_filtered_items = input.is_all_filtered_items || false;
1434

1535
if (configuration.native_search_enabled === false && input.query) {
@@ -178,6 +198,7 @@ export function sorted_items(items, sort, sortings) {
178198
* useful for autocomplete or list all aggregation options
179199
*/
180200
export function similar(items, id, options) {
201+
options = options || Object.create(null);
181202
const per_page = options.per_page || 10;
182203
const minimum = options.minimum || 0;
183204
const page = options.page || 1;
@@ -191,6 +212,19 @@ export function similar(items, id, options) {
191212
}
192213
}
193214

215+
if (!item) {
216+
return {
217+
pagination: {
218+
per_page: per_page,
219+
page: page,
220+
total: 0,
221+
},
222+
data: {
223+
items: [],
224+
},
225+
};
226+
}
227+
194228
if (!options.field) {
195229
throw new Error('Please define field in options');
196230
}
@@ -203,9 +237,10 @@ export function similar(items, id, options) {
203237
const intersection = _intersection(item[field], items[i][field]);
204238

205239
if (intersection.length >= minimum) {
206-
sorted_items.push(items[i]);
207-
sorted_items[sorted_items.length - 1].intersection_length =
208-
intersection.length;
240+
sorted_items.push({
241+
...items[i],
242+
intersection_length: intersection.length,
243+
});
209244
}
210245
}
211246
}
@@ -250,9 +285,18 @@ export function aggregation(items, input, configuration, fulltext, facets) {
250285
throw new Error('field name is required');
251286
}
252287

253-
configuration.aggregations[input.name].size = 10000;
288+
const aggregationConfig = {
289+
...configuration,
290+
aggregations: {
291+
...configuration.aggregations,
292+
[input.name]: {
293+
...configuration.aggregations[input.name],
294+
size: 10000,
295+
},
296+
},
297+
};
254298

255-
const result = search(items, search_input, configuration, fulltext, facets);
299+
const result = search(items, search_input, aggregationConfig, fulltext, facets);
256300
const buckets = result.data.aggregations[input.name].buckets;
257301

258302
return {

src/utils/facetsCore.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ export const matrix = function (facets, filters = []) {
135135
filters.forEach((filter) => {
136136
if (filter.length === 3 && filter[1] === '-') {
137137
const [filter_key, , filter_val] = filter;
138-
const negative_bits = temp_facet.bits_data_temp[filter_key][filter_val].clone();
138+
const negative_bits =
139+
temp_facet.bits_data_temp[filter_key]?.[filter_val]?.clone() ||
140+
new FastBitSet();
139141

140142
for (const key in temp_facet.bits_data_temp) {
141143
for (const key2 in temp_facet.bits_data_temp[key]) {

tests/browserifySpec.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,24 @@ describe('itemjs general tests', function () {
335335
done();
336336
});
337337

338+
it('returns empty list when similar base item is missing', function test(done) {
339+
const itemsWithIds = items.map((item, idx) => ({
340+
...item,
341+
id: idx + 1,
342+
}));
343+
const itemsjs = itemsJS(itemsWithIds, {
344+
aggregations: {
345+
tags: {},
346+
},
347+
});
348+
349+
const result = itemsjs.similar(999, { field: 'tags', per_page: 5 });
350+
351+
assert.equal(result.pagination.total, 0);
352+
assert.deepEqual(result.data.items, []);
353+
done();
354+
});
355+
338356
it('search by tags', function test(done) {
339357
const items = [
340358
{

tests/searchSpec.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,20 @@ describe('search', function () {
200200
done();
201201
});
202202

203+
it('ignores not filters for values not present in index', function test(done) {
204+
const itemsjs = itemsJS(items, configuration);
205+
206+
const result = itemsjs.search({
207+
not_filters: {
208+
tags: ['not-existing'],
209+
},
210+
});
211+
212+
assert.equal(result.data.items.length, 4);
213+
214+
done();
215+
});
216+
203217
it('marks boolean facets as selected', function test(done) {
204218
const dataset = [
205219
{ boolean: true, string: 'true' },
@@ -230,6 +244,72 @@ describe('search', function () {
230244
done();
231245
});
232246

247+
it('aggregation does not mutate configured facet size', function test(done) {
248+
const smallDataset = [
249+
{ id: 1, tags: ['a'] },
250+
{ id: 2, tags: ['b'] },
251+
{ id: 3, tags: ['c'] },
252+
];
253+
254+
const itemsjs = itemsJS(smallDataset, {
255+
aggregations: {
256+
tags: { size: 1 },
257+
},
258+
});
259+
260+
const initial = itemsjs.search({});
261+
assert.equal(initial.data.aggregations.tags.buckets.length, 1);
262+
263+
const aggResult = itemsjs.aggregation({ name: 'tags', per_page: 10 });
264+
assert.equal(aggResult.data.buckets.length, 3);
265+
266+
const after = itemsjs.search({});
267+
assert.equal(after.data.aggregations.tags.buckets.length, 1);
268+
269+
done();
270+
});
271+
272+
it('normalizes pagination values to safe defaults', function test(done) {
273+
const dataset = Array.from({ length: 15 }, (_, idx) => ({
274+
id: idx + 1,
275+
tags: ['t' + (idx % 3)],
276+
}));
277+
278+
const itemsjs = itemsJS(dataset, {
279+
aggregations: {
280+
tags: {},
281+
},
282+
});
283+
284+
const result = itemsjs.search({
285+
per_page: Infinity,
286+
page: -5,
287+
});
288+
289+
assert.equal(result.pagination.page, 1);
290+
assert.equal(result.pagination.per_page, 12);
291+
assert.equal(result.data.items.length, 12);
292+
293+
const resultString = itemsjs.search({
294+
per_page: 'abc',
295+
page: 'not-a-number',
296+
});
297+
298+
assert.equal(resultString.pagination.page, 1);
299+
assert.equal(resultString.pagination.per_page, 12);
300+
assert.equal(resultString.data.items.length, 12);
301+
302+
const zeroPerPage = itemsjs.search({
303+
per_page: 0,
304+
page: 3,
305+
});
306+
assert.equal(zeroPerPage.pagination.per_page, 0);
307+
assert.equal(zeroPerPage.pagination.page, 1);
308+
assert.equal(zeroPerPage.data.items.length, 0);
309+
310+
done();
311+
});
312+
233313
it('makes search with non existing filter value with conjunction true should return no results', function test(done) {
234314
const itemsjs = itemsJS(items, configuration);
235315

0 commit comments

Comments
 (0)