feat: add search query domain rules
This commit is contained in:
@@ -0,0 +1,70 @@
|
|||||||
|
function requiredString(value, fieldName) {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
throw new Error(`${fieldName} must be a string.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeSearchQuery(value) {
|
||||||
|
return requiredString(value, 'Search query')
|
||||||
|
.trim()
|
||||||
|
.normalize('NFKC')
|
||||||
|
.replace(/[^\p{L}\p{N}\s]+/gu, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSearchQuery({
|
||||||
|
query = '',
|
||||||
|
category,
|
||||||
|
minPriceMinor,
|
||||||
|
maxPriceMinor,
|
||||||
|
page = 1,
|
||||||
|
limit = 20,
|
||||||
|
includeSold = false,
|
||||||
|
}) {
|
||||||
|
const text = normalizeSearchQuery(query);
|
||||||
|
|
||||||
|
if (!Number.isInteger(page) || page < 1) {
|
||||||
|
throw new Error('page must be >= 1');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
||||||
|
throw new Error('limit must be between 1 and 100');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof minPriceMinor !== 'undefined' && (!Number.isInteger(minPriceMinor) || minPriceMinor < 0)) {
|
||||||
|
throw new Error('minPriceMinor must be a non-negative integer');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof maxPriceMinor !== 'undefined' && (!Number.isInteger(maxPriceMinor) || maxPriceMinor < 0)) {
|
||||||
|
throw new Error('maxPriceMinor must be a non-negative integer');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof minPriceMinor !== 'undefined' &&
|
||||||
|
typeof maxPriceMinor !== 'undefined' &&
|
||||||
|
minPriceMinor > maxPriceMinor
|
||||||
|
) {
|
||||||
|
throw new Error('minPriceMinor must be <= maxPriceMinor');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
text,
|
||||||
|
...(typeof category === 'string' && category.trim().length > 0 ? { category: category.trim() } : {}),
|
||||||
|
...(typeof minPriceMinor === 'number' ? { minPriceMinor } : {}),
|
||||||
|
...(typeof maxPriceMinor === 'number' ? { maxPriceMinor } : {}),
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
includeSold,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSearchFilters({ includeSold = false, status = 'AVAILABLE', sort = ['recently_added'] }) {
|
||||||
|
return {
|
||||||
|
status: includeSold ? 'ALL' : status,
|
||||||
|
includeSold,
|
||||||
|
sort,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildSearchFilters,
|
||||||
|
createSearchQuery,
|
||||||
|
normalizeSearchQuery,
|
||||||
|
} from '../../src/modules/search/domain/search-query.js';
|
||||||
|
|
||||||
|
test('normalizes a public search query and keeps filter values within bounds', () => {
|
||||||
|
const query = createSearchQuery({
|
||||||
|
query: ' Charizard ex ',
|
||||||
|
category: 'Cards',
|
||||||
|
minPriceMinor: 1000,
|
||||||
|
maxPriceMinor: 5000,
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
includeSold: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(query.text, 'charizard ex');
|
||||||
|
assert.equal(query.category, 'Cards');
|
||||||
|
assert.equal(query.minPriceMinor, 1000);
|
||||||
|
assert.equal(query.maxPriceMinor, 5000);
|
||||||
|
assert.equal(query.page, 1);
|
||||||
|
assert.equal(query.limit, 20);
|
||||||
|
assert.equal(query.includeSold, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects invalid pagination and price boundaries', () => {
|
||||||
|
assert.throws(() => createSearchQuery({ query: 'abc', page: 0, limit: 20 }), /page must be >= 1/);
|
||||||
|
assert.throws(() => createSearchQuery({ query: 'abc', minPriceMinor: 5000, maxPriceMinor: 1000 }), /minPriceMinor must be <= maxPriceMinor/);
|
||||||
|
assert.throws(() => createSearchQuery({ query: 'abc', limit: 0 }), /limit must be between 1 and 100/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('builds a public query filter set that hides sold items by default', () => {
|
||||||
|
const filters = buildSearchFilters({ includeSold: false });
|
||||||
|
|
||||||
|
assert.equal(filters.status, 'AVAILABLE');
|
||||||
|
assert.equal(filters.includeSold, false);
|
||||||
|
assert.deepEqual(filters.sort, ['recently_added']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normalizes whitespace and punctuation in free-text search terms', () => {
|
||||||
|
assert.equal(normalizeSearchQuery(' Pikachu #199 / holo '), 'pikachu 199 holo');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user