@mohasinac/appkit 4.30.1 → 4.30.2
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.
|
@@ -116,6 +116,17 @@ export interface PublicProductQuery {
|
|
|
116
116
|
page: number;
|
|
117
117
|
pageSize: number;
|
|
118
118
|
cursor?: string | null;
|
|
119
|
+
/**
|
|
120
|
+
* Free-text search, carried OUTSIDE `filters` on purpose.
|
|
121
|
+
*
|
|
122
|
+
* `searchTxt` matching is `array-contains`, which Sieve cannot express — so
|
|
123
|
+
* it travels as an opt to `productRepository.list` (and as `baseOpts.search`
|
|
124
|
+
* to the listingProcessor Function, whose products lister forwards opts
|
|
125
|
+
* wholesale). Every executor must pass it on; dropping it is silent, because
|
|
126
|
+
* a query with no search term is still a perfectly valid query that returns
|
|
127
|
+
* rows.
|
|
128
|
+
*/
|
|
129
|
+
search?: string;
|
|
119
130
|
}
|
|
120
131
|
export interface ExecutorResult {
|
|
121
132
|
items: unknown[];
|
|
@@ -275,8 +275,18 @@ function buildFirestoreSafeFilters(input) {
|
|
|
275
275
|
if (input.prizeRevealStatus) {
|
|
276
276
|
parts.push(sieveFilter(PRODUCT_FIELDS.PRIZE_REVEAL_STATUS, SIEVE_OP.EQ, input.prizeRevealStatus));
|
|
277
277
|
}
|
|
278
|
-
|
|
279
|
-
|
|
278
|
+
// `input.q` is deliberately NOT a filter clause.
|
|
279
|
+
//
|
|
280
|
+
// It used to be `title@=*<q>` pushed HERE — before the features, tags,
|
|
281
|
+
// sublisting and typeFacets clauses below. The Firebase adapter throws on
|
|
282
|
+
// case-insensitive operators and `throwExceptions: false` swallowed it, so
|
|
283
|
+
// the processor returned the query as of the throw: with this clause first,
|
|
284
|
+
// NOTHING was applied. Typing a search term discarded every facet the user
|
|
285
|
+
// had selected AND the sort, and answered 200 with the whole catalogue.
|
|
286
|
+
//
|
|
287
|
+
// Search now travels as `query.search` (see PublicProductQuery) and becomes an
|
|
288
|
+
// `array-contains` on `searchTxt` inside the repository, which is the only
|
|
289
|
+
// shape Firestore can serve for token matching.
|
|
280
290
|
// `array-contains-any` isn't supported by the Sieve Firebase adapter, so only a
|
|
281
291
|
// single feature can be pushed down; multi-select falls through to the caller.
|
|
282
292
|
if (input.features?.length === 1) {
|
|
@@ -329,7 +339,11 @@ async function defaultExecutor(query) {
|
|
|
329
339
|
sorts: query.sorts,
|
|
330
340
|
page: query.page,
|
|
331
341
|
pageSize: query.pageSize,
|
|
332
|
-
}
|
|
342
|
+
},
|
|
343
|
+
// Without this second argument the repository's searchTxt read path is
|
|
344
|
+
// unreachable on the default executor, which is what every SSR listing view
|
|
345
|
+
// uses. It was implemented and simply never called.
|
|
346
|
+
query.search ? { search: query.search } : undefined);
|
|
333
347
|
return {
|
|
334
348
|
items: result.items,
|
|
335
349
|
total: result.total,
|
|
@@ -405,6 +419,7 @@ export async function listPublicProducts(input, opts) {
|
|
|
405
419
|
page,
|
|
406
420
|
pageSize,
|
|
407
421
|
cursor: input.cursor ?? null,
|
|
422
|
+
search: input.q,
|
|
408
423
|
});
|
|
409
424
|
if (!result)
|
|
410
425
|
return null;
|
|
@@ -440,6 +455,7 @@ export async function listPublicProducts(input, opts) {
|
|
|
440
455
|
page: 1,
|
|
441
456
|
pageSize: PUBLIC_PRODUCT_MAX_PAGE_SIZE,
|
|
442
457
|
cursor: null,
|
|
458
|
+
search: input.q,
|
|
443
459
|
});
|
|
444
460
|
}));
|
|
445
461
|
// One failed branch must not blank the whole scope — but it is already
|
|
@@ -505,6 +521,7 @@ export async function listPublicProducts(input, opts) {
|
|
|
505
521
|
page: inMemory ? 1 : page,
|
|
506
522
|
pageSize: inMemory ? PUBLIC_PRODUCT_MAX_PAGE_SIZE : pageSize,
|
|
507
523
|
cursor: inMemory ? null : (input.cursor ?? null),
|
|
524
|
+
search: input.q,
|
|
508
525
|
});
|
|
509
526
|
if (!result)
|
|
510
527
|
return null;
|
|
@@ -5,7 +5,7 @@ import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../..
|
|
|
5
5
|
import { cacheManager } from "../../../core";
|
|
6
6
|
import { serverLogger } from "../../../monitoring";
|
|
7
7
|
import { generateUniqueId, slugify, generateBarcodeId } from "../../../utils";
|
|
8
|
-
import { buildSearchTxt, parseSearchTxtQuery } from "../../../utils/search-txt";
|
|
8
|
+
import { buildSearchTxt, matchesAllSearchTerms, parseSearchTxtQuery, } from "../../../utils/search-txt";
|
|
9
9
|
import { PRODUCT_COLLECTION, ProductStatusValues } from "../schemas";
|
|
10
10
|
import { PRODUCT_FIELDS } from "../../../constants/field-names";
|
|
11
11
|
/**
|
|
@@ -408,12 +408,36 @@ export class ProductRepository extends BaseRepository {
|
|
|
408
408
|
return baseQuery;
|
|
409
409
|
}
|
|
410
410
|
async list(model, opts) {
|
|
411
|
-
|
|
411
|
+
const result = await this.sieveQuery(model, ProductRepository.SIEVE_FIELDS, {
|
|
412
412
|
baseQuery: this.buildScopedQuery(opts),
|
|
413
413
|
defaultPageSize: 20,
|
|
414
414
|
maxPageSize: 100,
|
|
415
415
|
aliases: ProductRepository.FILTER_ALIASES,
|
|
416
416
|
});
|
|
417
|
+
// Only the most selective term became the `array-contains` clause, because
|
|
418
|
+
// Firestore permits one array operator per query. The REST of the terms
|
|
419
|
+
// were described as "AND-refined" in buildScopedQuery for a long time while
|
|
420
|
+
// nothing actually refined them — so "red dranzer" returned everything
|
|
421
|
+
// matching "dranzer" and the second word did nothing at all.
|
|
422
|
+
//
|
|
423
|
+
// Same shape as faqsRepository.list, deliberately: one implementation of
|
|
424
|
+
// "AND the remaining terms" that both collections read the same way.
|
|
425
|
+
const terms = parseSearchTxtQuery(opts?.search ?? "");
|
|
426
|
+
const extraTerms = terms.slice(1);
|
|
427
|
+
if (extraTerms.length === 0)
|
|
428
|
+
return result;
|
|
429
|
+
const items = result.items.filter((p) => matchesAllSearchTerms(p.searchTxt, extraTerms));
|
|
430
|
+
// `total` becomes a FLOOR once rows are dropped after the page was cut —
|
|
431
|
+
// it counts this page only. That is the known post-pagination-filter debt
|
|
432
|
+
// (Phase 5), not a new invention: reporting the pre-filter total here would
|
|
433
|
+
// promise pages that render empty.
|
|
434
|
+
return {
|
|
435
|
+
...result,
|
|
436
|
+
items,
|
|
437
|
+
total: items.length,
|
|
438
|
+
totalPages: items.length === 0 ? 0 : 1,
|
|
439
|
+
hasMore: false,
|
|
440
|
+
};
|
|
417
441
|
}
|
|
418
442
|
/**
|
|
419
443
|
* How many products of each listing type exist within a scope — the numbers
|