Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions main/routes/orders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,19 +675,20 @@ router.get(
)
)`;

const isEligible = [...search].length >= 3 && (!isPhoneSearch || phoneDigits.length >= 3);
const hasControlChars = /[\x00-\x1F\x7F]/.test(search);
const isEligible = !hasControlChars && [...search].length >= 3;
const hasFts = isEligible && isSearchIndexAvailable(db);

if (hasFts) {
let candidateClause: string;
const candidateParams: any[] = [];
const ftsSearch = '"' + search.replace(/"/g, '""') + '"';

if (isPhoneSearch) {
// For phone searches, customer phone strings may contain varied formatting
// (e.g. "+1/555 ext 0199"). Querying customers table with digit UDF efficiently
// identifies matching customer IDs across the small customers table, avoiding a
// full orders scan while preserving full parity across phone formats.
const ftsSearch = '"' + phoneDigits.replace(/"/g, '""') + '"';
candidateClause = `(
orders.id IN (SELECT rowid FROM orders_search_fts WHERE orders_search_fts MATCH ?)
OR orders.customer_id IN (
Expand All @@ -699,7 +700,6 @@ router.get(
)`;
candidateParams.push(ftsSearch, phoneDigits, phoneDigits, search);
} else {
const ftsSearch = '"' + search.replace(/"/g, '""') + '"';
candidateClause = `(
orders.id IN (SELECT rowid FROM orders_search_fts WHERE orders_search_fts MATCH ?)
OR orders.customer_id IN (
Expand Down
109 changes: 109 additions & 0 deletions tests/orders-search-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ async function main() {
insertOrder.run('ORD-SYNTH-NOCUST-999', null, '2023-01-01 00:00:00', '2023-01-01 00:00:00');
insertOrder.run('ORD-SYNTH-ALPHA123', null, '2023-01-01 00:00:00', '2023-01-01 00:00:00');
insertOrder.run('ORD-SYNTH-EMOJI-🍕🍔', null, '2023-01-01 00:00:00', '2023-01-01 00:00:00');
insertOrder.run('01-1234', null, '2023-01-01 00:00:00', '2023-01-01 00:00:00');
insertOrder.run('2026-0042', null, '2023-01-01 00:00:00', '2023-01-01 00:00:00');
insertOrder.run('#100-special', null, '2023-01-01 00:00:00', '2023-01-01 00:00:00');
insertOrder.run('ORD-SYNTH-TAB\t123', null, '2023-01-01 00:00:00', '2023-01-01 00:00:00');
for (let i = 0; i < 55; i++) {
insertOrder.run(
`ORD-SEARCH-NEWER-${i}`,
Expand Down Expand Up @@ -356,6 +360,111 @@ async function main() {
);
}

// 12. Hyphenated numeric order number probe
const probeHyphenOrder = await api(
baseUrl,
`/api/orders?search=${encodeURIComponent('01-1234')}`,
{ headers: owner.authHeader },
);
assertEqualOrThrow(
probeHyphenOrder.status,
200,
'probe search for hyphenated order number succeeds',
);
assertOrThrow(
probeHyphenOrder.data.orders.some((o: any) => o.order_number === '01-1234'),
'probe returns order matching hyphenated order number 01-1234',
);

// 13. Year-hyphenated order number and partial numeric sequence probe
const probeYearOrder = await api(
baseUrl,
`/api/orders?search=${encodeURIComponent('2026-0042')}`,
{ headers: owner.authHeader },
);
assertEqualOrThrow(probeYearOrder.status, 200, 'probe search for 2026-0042 succeeds');
assertOrThrow(
probeYearOrder.data.orders.some((o: any) => o.order_number === '2026-0042'),
'probe returns order matching 2026-0042',
);

const probePartialHyphen = await api(
baseUrl,
`/api/orders?search=${encodeURIComponent('01-12')}`,
{ headers: owner.authHeader },
);
assertEqualOrThrow(
probePartialHyphen.status,
200,
'probe search for partial hyphen sequence succeeds',
);
assertOrThrow(
probePartialHyphen.data.orders.some((o: any) => o.order_number === '01-1234'),
'probe returns order matching partial hyphen sequence 01-12',
);

const probeNumericSub = await api(baseUrl, `/api/orders?search=${encodeURIComponent('1234')}`, {
headers: owner.authHeader,
});
assertEqualOrThrow(probeNumericSub.status, 200, 'probe search for numeric substring succeeds');
assertOrThrow(
probeNumericSub.data.orders.some((o: any) => o.order_number === '01-1234'),
'probe returns order matching numeric substring 1234',
);

// 14. Special punctuation order number probe
const probeSpecialPunct = await api(
baseUrl,
`/api/orders?search=${encodeURIComponent('#100-')}`,
{ headers: owner.authHeader },
);
assertEqualOrThrow(probeSpecialPunct.status, 200, 'probe search for #100- succeeds');
assertOrThrow(
probeSpecialPunct.data.orders.some((o: any) => o.order_number === '#100-special'),
'probe returns order matching #100-special',
);

// 15. Embedded NUL character probe
const probeNul = await api(baseUrl, `/api/orders?search=${encodeURIComponent('01-1234\0')}`, {
headers: owner.authHeader,
});
assertEqualOrThrow(probeNul.status, 200, 'probe search with embedded NUL succeeds without 500');

// 16. Control characters probe
const probeControl = await api(
baseUrl,
`/api/orders?search=${encodeURIComponent('\x01\x02test')}`,
{ headers: owner.authHeader },
);
assertEqualOrThrow(
probeControl.status,
200,
'probe search with control chars succeeds without 500',
);

// 17. Embedded tab, newline, carriage return, and DEL probe
const probeTab = await api(
baseUrl,
`/api/orders?search=${encodeURIComponent('SYNTH-TAB\t123')}`,
{ headers: owner.authHeader },
);
assertEqualOrThrow(probeTab.status, 200, 'probe search with embedded tab succeeds without 500');
assertOrThrow(
probeTab.data.orders.some((o: any) => o.order_number === 'ORD-SYNTH-TAB\t123'),
'probe returns order matching embedded tab via UDF fallback',
);

const probeNewlineCrDel = await api(
baseUrl,
`/api/orders?search=${encodeURIComponent('test\n\r\x7F')}`,
{ headers: owner.authHeader },
);
assertEqualOrThrow(
probeNewlineCrDel.status,
200,
'probe search with newline, carriage return, and DEL succeeds without 500',
);

const activePage1 = await api(
baseUrl,
'/api/orders?view=active&type=takeaway&status=pending&per_page=15',
Expand Down
Loading