This commit is contained in:
David Arranz 2026-08-03 13:37:40 +02:00
parent 7d61dd7598
commit 163f2b9fa4
5 changed files with 86 additions and 6 deletions

View File

@ -0,0 +1,20 @@
import { describe, expect, test } from "vitest";
import { buildBooleanFullTextQuery } from "../build-boolean-full-text-query";
describe("buildBooleanFullTextQuery", () => {
test.each([
["pr", "+pr*"],
["pr-", "+pr*"],
["-pr", "+pr*"],
["cliente prueba", "+cliente* +prueba*"],
["cliente-prueba", "+cliente* +prueba*"],
["José Pérez", "+José* +Pérez*"],
["++ -- **", ""],
["", ""],
[" ", ""],
['+ - > < ( ) ~ * " @', ""],
])("normalizes %p into %p", (input, expected) => {
expect(buildBooleanFullTextQuery(input)).toBe(expected);
});
});

View File

@ -0,0 +1,53 @@
import { describe, expect, test } from "vitest";
import { Op, type Sequelize } from "sequelize";
import { Criteria } from "../critera";
import { CriteriaToSequelizeConverter } from "../criteria-to-sequelize-converter";
const createDatabaseStub = () =>
({
escape: (value: string) => `'${value.replaceAll("'", "''")}'`,
}) as Sequelize;
describe("CriteriaToSequelizeConverter quick search", () => {
test("builds a full-text filter and score for free text search", () => {
const converter = new CriteriaToSequelizeConverter();
const criteria = Criteria.fromPrimitives([], "name", "asc", 10, 0, "pr-");
const options = converter.convert(criteria, {
searchableFields: ["name", "trade_name"],
sortableFields: ["name"],
enableFullText: true,
database: createDatabaseStub(),
strictMode: true,
});
expect(options.attributes).toEqual({
include: [[expect.objectContaining({ val: expect.stringContaining("'+pr*'") }), "score"]],
});
expect(options.order).toEqual([
[expect.objectContaining({ val: "score" }), "DESC"],
["name", "ASC"],
]);
expect(options.where).toEqual({
[Op.and]: expect.any(Array),
});
});
test("omits MATCH, score and score ordering when the normalized search is empty", () => {
const converter = new CriteriaToSequelizeConverter();
const criteria = Criteria.fromPrimitives([], "name", "asc", 10, 0, "++ -- **");
const options = converter.convert(criteria, {
searchableFields: ["name", "trade_name"],
sortableFields: ["name"],
enableFullText: true,
database: createDatabaseStub(),
strictMode: true,
});
expect(options.attributes).toBeUndefined();
expect(options.order).toEqual([["name", "ASC"]]);
expect(options.where).toBeUndefined();
});
});

View File

@ -0,0 +1,9 @@
const FULL_TEXT_TOKEN_PATTERN = /[\p{L}\p{N}]+/gu;
export function buildBooleanFullTextQuery(search: string): string {
if (!search) return "";
const terms = search.normalize("NFKC").match(FULL_TEXT_TOKEN_PATTERN) ?? [];
return terms.map((term) => `+${term}*`).join(" ");
}

View File

@ -7,6 +7,7 @@ import type {
CriteriaMappings,
ICriteriaToOrmConverter,
} from "./types.js";
import { buildBooleanFullTextQuery } from "./build-boolean-full-text-query.js";
import { appendOrder, normalizeOrder, prependOrder } from "./utils";
/**
@ -66,11 +67,8 @@ export class CriteriaToSequelizeConverter implements ICriteriaToOrmConverter {
return;
}
const booleanTerm = term
.split(/\s+/)
.filter(Boolean)
.map((word) => `+${word}*`)
.join(" ");
const booleanTerm = buildBooleanFullTextQuery(term);
if (!booleanTerm) return;
const qualifiedFields = searchableFields.map((field) =>
this.qualifyRootColumn(field, fullTextTableAlias)
@ -216,7 +214,6 @@ export class CriteriaToSequelizeConverter implements ICriteriaToOrmConverter {
return Op.lt;
case "LOWER_THAN_OR_EQUAL":
return Op.lte;
case "EQUALS":
default:
return Op.eq;
}

View File

@ -1,4 +1,5 @@
export * from "./critera";
export * from "./build-boolean-full-text-query";
export * from "./criteria-from-url-converter";
export * from "./criteria-to-sequelize-converter";
export * from "./defaults";