@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
- if (input.q)
279
- parts.push(sieveFilter(PRODUCT_FIELDS.TITLE, SIEVE_OP.CONTAINS_CI, input.q));
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
- return this.sieveQuery(model, ProductRepository.SIEVE_FIELDS, {
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.30.1",
3
+ "version": "4.30.2",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"