@mohasinac/appkit 4.32.1 → 4.32.3

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.
@@ -149,9 +149,11 @@ export async function resolveBundleCategorySlugs(productIds) {
149
149
  */
150
150
  export const listFeaturedBundles = cache(async (limit = 8, _opts) => {
151
151
  void _opts;
152
- const all = await categoriesRepository
153
- .listByType("bundle", { activeOnly: true, limit })
154
- .catch(() => []);
152
+ const all = await safeRead(() => categoriesRepository.listByType("bundle", { activeOnly: true, limit }), {
153
+ route: "bundles/getFeaturedBundles",
154
+ key: "categories.listByType(bundle)",
155
+ fallback: [],
156
+ });
155
157
  return all;
156
158
  });
157
159
  /**
@@ -163,8 +165,13 @@ export const listFeaturedBundles = cache(async (limit = 8, _opts) => {
163
165
  * detail pages had no related section at all before this.
164
166
  */
165
167
  export const getRelatedBundles = cache(async (bundle, limit = 8) => {
166
- const all = await categoriesRepository
167
- .listByType("bundle", { activeOnly: true, limit: limit + 1 })
168
- .catch(() => []);
168
+ const all = await safeRead(() => categoriesRepository.listByType("bundle", {
169
+ activeOnly: true,
170
+ limit: limit + 1,
171
+ }), {
172
+ route: "bundles/getRelatedBundles",
173
+ key: "categories.listByType(bundle)",
174
+ fallback: [],
175
+ });
169
176
  return all.filter((b) => b.id !== bundle.id).slice(0, limit);
170
177
  });
@@ -442,6 +442,21 @@ export async function listPublicProducts(input, opts) {
442
442
  ? unavailableClausesFor(types).find((c) => c.op === "lt" && c.sortField === requestedSortField)
443
443
  : undefined;
444
444
  if (pushdown) {
445
+ /*
446
+ * 🛑 This branch keeps a POST-pagination sandbox filter, knowingly.
447
+ *
448
+ * It exists to give the ended-listing archive real, unbounded-depth
449
+ * Firestore pagination, which is incompatible with the over-fetch the
450
+ * window path uses. So an anonymous page here can come back a row or two
451
+ * SHORT when a sandbox fixture lands in it — bounded by the ~25 fixtures
452
+ * that exist, and nothing like the empty page the default view produced,
453
+ * because real ended listings dominate this scope.
454
+ *
455
+ * Falling through to Path C instead would fix the short page and cost every
456
+ * anonymous visitor the deep pagination this branch was written for. The
457
+ * short page is the better trade; it is recorded here so the next reader
458
+ * knows it was weighed rather than missed.
459
+ */
445
460
  const filters = sieveAnd(safeFilters, clauseToSieve(pushdown, now));
446
461
  const result = await runQuery(executor, {
447
462
  filters,
@@ -558,7 +573,28 @@ export async function listPublicProducts(input, opts) {
558
573
  const hasTypeFacet = Object.keys(input.typeFacets ?? {}).length > 0;
559
574
  /** Multi-value `tags`/`features` — no `array-contains-any`, so per-row. */
560
575
  const hasArrayMultiSelect = (input.tags?.length ?? 0) > 1 || (input.features?.length ?? 0) > 1;
561
- const inMemory = wantAvailable || hasTypeFacet || hasArrayMultiSelect || (hasDateRange && !canPushDate);
576
+ /*
577
+ * 🛑 The tester-sandbox filter must run BEFORE the page is sliced.
578
+ *
579
+ * It used to run only in `finish()`, which receives an already-paginated
580
+ * page — so a page whose rows were ALL sandbox fixtures came back empty with
581
+ * a non-zero `total`. That is not hypothetical: the fixtures carry
582
+ * `Date.now()`-relative dates by design (they have to re-arm on every
583
+ * reseed), so immediately after a seed load they are the newest rows in the
584
+ * collection and fill the whole first page of the default `-createdAt` sort.
585
+ * The public catalogue returned `items: []` alongside `total: 79`.
586
+ *
587
+ * So an anonymous viewer forces the bounded-window path, where the filter is
588
+ * one more per-row predicate applied before the slice. That is already the
589
+ * default for public browsing (`wantAvailable`), so this widens the window
590
+ * path only for the non-default scopes.
591
+ */
592
+ const hidesTestData = filterTestDataForViewer([{ isTestData: true }], opts?.viewer).length === 0;
593
+ const inMemory = wantAvailable ||
594
+ hasTypeFacet ||
595
+ hasArrayMultiSelect ||
596
+ hidesTestData ||
597
+ (hasDateRange && !canPushDate);
562
598
  // When a date range stays in memory, fetch in whichever direction front-loads
563
599
  // the rows that will PASS it: `dateFrom` (">=", still live) wants the most
564
600
  // future dates first; `dateTo` ("<=", ends by) wants the earliest first.
@@ -597,6 +633,10 @@ export async function listPublicProducts(input, opts) {
597
633
  return wanted.some((w) => raw.includes(w));
598
634
  };
599
635
  const filtered = items.filter((item) => {
636
+ // Before the slice — see the note on `hidesTestData` above.
637
+ if (hidesTestData && item.isTestData === true) {
638
+ return false;
639
+ }
600
640
  if (wantAvailable && !isListingRowAvailable(item, now))
601
641
  return false;
602
642
  if (hasArrayMultiSelect) {
@@ -3,6 +3,7 @@ import { cache } from "react";
3
3
  import { storeRepository } from "../../../../repositories";
4
4
  import { STORES_PRODUCTS_PAGE_SIZE, STORES_SITEMAP_LIMIT, } from "../../../shared/features/stores/config";
5
5
  import { makeGetStoreListingsInitial } from "../shared/listing-data-factory";
6
+ import { safeRead } from "../../../../errors/safe-read";
6
7
  /** Full store document by slug — deduped per request via React.cache(). */
7
8
  export const getStoreForDetail = cache(async (slug) => {
8
9
  return (await storeRepository.findBySlug(slug)) ?? null;
@@ -13,8 +14,10 @@ export const listStoreAuctionsInitial = makeGetStoreListingsInitial("auction", S
13
14
  export const listStorePreOrdersInitial = makeGetStoreListingsInitial("pre-order", STORES_PRODUCTS_PAGE_SIZE);
14
15
  /** Stores for sitemap generation. */
15
16
  export const listSitemapStores = cache(async () => {
16
- const result = await storeRepository
17
- .listStores({ filters: "status==active", sorts: "-updatedAt", page: 1, pageSize: STORES_SITEMAP_LIMIT }, true)
18
- .catch(() => null);
17
+ /*
18
+ * The SITEMAP. A swallowed failure here publishes a sitemap missing every
19
+ * store — which is not an error to any crawler, just a smaller site.
20
+ */
21
+ const result = await safeRead(() => storeRepository.listStores({ filters: "status==active", sorts: "-updatedAt", page: 1, pageSize: STORES_SITEMAP_LIMIT }, true), { route: "sitemap/stores", key: "stores.listStores", fallback: null });
19
22
  return (result?.items ?? []).map(({ storeSlug, updatedAt }) => ({ storeSlug, updatedAt }));
20
23
  });
@@ -28,6 +28,7 @@ import { getDefaultCurrency } from "../../../core/baseline-resolver";
28
28
  import { cartRepository } from "../../cart/repository/cart.repository";
29
29
  import { AUCTION_BUYOUT_WINDOW_MINUTES, AUCTION_BUYOUT_WINDOW_MS, AUCTION_CHECKOUT_URL, } from "../../../_internal/shared/checkout/lanes";
30
30
  import { resolveDate } from "../../../utils";
31
+ import { safeRead } from "../../../errors/safe-read";
31
32
  // --- Domain Functions ----------------------------------------------------------
32
33
  /**
33
34
  * Load the bidder's profile and refuse if they carry an active `place_bids`
@@ -110,9 +111,9 @@ export async function placeBid(userId, userEmail, input) {
110
111
  : BID_ERROR_CODES.TOO_LOW,
111
112
  });
112
113
  }
113
- const previousWinner = await bidRepository
114
- .getWinningBid(productId)
115
- .catch(() => null);
114
+ // A failure here is not "no previous winner" — it silently uncaps the
115
+ // auto-bid ceiling below, which is a money path.
116
+ const previousWinner = await safeRead(() => bidRepository.getWinningBid(productId), { route: "bid-actions/placeBid", key: "bids.getWinningBid", fallback: null });
116
117
  const prevCap = previousWinner?.data
117
118
  ? (previousWinner.data.autoMaxBid ?? previousWinner.data.bidAmount ?? baseBid)
118
119
  : baseBid;
@@ -132,9 +132,11 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
132
132
  : [];
133
133
  const descriptionHtml = toDescriptionHtml(p.description);
134
134
  const sublistingCategoryId = typeof p.sublistingCategoryId === "string" ? p.sublistingCategoryId : null;
135
- const relatedDocs = await productRepository
136
- .findByCategory(String(p.category ?? ""))
137
- .catch(() => []);
135
+ const relatedDocs = (await safeRead(() => productRepository.findByCategory(String(p.category ?? "")), {
136
+ route: "/auctions/[slug]",
137
+ key: "products.findByCategory",
138
+ fallback: [],
139
+ }));
138
140
  const [{ relatedItems, relatedByBrand, relatedByTags, relatedByStore }, groups, siteSettings] = await Promise.all([
139
141
  computeRelatedItems(product),
140
142
  getGroupsWithItemsForProduct(product.id),
@@ -3,6 +3,7 @@ import { blogRepository } from "../../../repositories";
3
3
  import { Container, Heading, Main, Section } from "../../../ui";
4
4
  import { AdSlot } from "../../homepage/components/AdSlot";
5
5
  import { BlogIndexListing } from "./BlogIndexListing";
6
+ import { safeRead } from "../../../errors/safe-read";
6
7
  function sp(params, key) {
7
8
  const v = params[key];
8
9
  return Array.isArray(v) ? v[0] ?? "" : v ?? "";
@@ -12,9 +13,7 @@ export async function BlogIndexPageView({ searchParams = {} }) {
12
13
  const page = Number(sp(searchParams, "page")) || 1;
13
14
  const pageSize = Number(sp(searchParams, "pageSize")) || 24;
14
15
  const category = sp(searchParams, "category");
15
- const sieveResult = await blogRepository
16
- .listPublished({ ...(category ? { category } : {}) }, { sorts: sort, page, pageSize })
17
- .catch(() => null);
16
+ const sieveResult = await safeRead(() => blogRepository.listPublished({ ...(category ? { category } : {}) }, { sorts: sort, page, pageSize }), { route: "/blog", key: "blogPosts.listPublished", fallback: null });
18
17
  const initialData = sieveResult
19
18
  ? {
20
19
  posts: sieveResult.items,
@@ -3,6 +3,7 @@ import { categoriesRepository } from "../../../repositories";
3
3
  import { Container, Heading, Main, Section, Text } from "../../../ui";
4
4
  import { AdSlot } from "../../homepage/components/AdSlot";
5
5
  import { CategoryBundlesListing } from "./CategoryBundlesListing";
6
+ import { safeRead } from "../../../errors/safe-read";
6
7
  const DEFAULT_LIMIT = 200;
7
8
  function sp(params, key) {
8
9
  const v = params[key];
@@ -17,9 +18,10 @@ function sp(params, key) {
17
18
  */
18
19
  export async function BundlesListView({ searchParams = {}, onBuyNow, }) {
19
20
  const storeId = sp(searchParams, "storeId");
20
- const all = await categoriesRepository
21
- .listByType("bundle", { activeOnly: true, limit: DEFAULT_LIMIT })
22
- .catch(() => []);
21
+ const all = await safeRead(() => categoriesRepository.listByType("bundle", {
22
+ activeOnly: true,
23
+ limit: DEFAULT_LIMIT,
24
+ }), { route: "/bundles", key: "categories.listByType(bundle)", fallback: [] });
23
25
  const bundles = storeId
24
26
  ? all.filter((c) => c.createdByStoreId === storeId)
25
27
  : all;
@@ -4,6 +4,7 @@ import { reviewRepository } from "../../../repositories";
4
4
  import { Container, Heading, Main, Section } from "../../../ui";
5
5
  import { AdSlot } from "../../homepage/components/AdSlot";
6
6
  import { ReviewsIndexListing } from "./ReviewsIndexListing";
7
+ import { safeRead } from "../../../errors/safe-read";
7
8
  function sp(params, key) {
8
9
  const v = params[key];
9
10
  return Array.isArray(v) ? v[0] ?? "" : v ?? "";
@@ -39,9 +40,7 @@ export async function ReviewsIndexPageView({ searchParams = {} }) {
39
40
  const sort = sp(searchParams, "sort") || "-createdAt";
40
41
  const page = Number(sp(searchParams, "page")) || 1;
41
42
  const filters = buildReviewFilters(searchParams);
42
- const result = await reviewRepository
43
- .listAll({ filters, sorts: sort, page, pageSize: 12 })
44
- .catch(() => null);
43
+ const result = await safeRead(() => reviewRepository.listAll({ filters, sorts: sort, page, pageSize: 12 }), { route: "/reviews", key: "reviews.listAll", fallback: null });
45
44
  const initialData = result
46
45
  ? {
47
46
  items: result.items,
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { categoriesRepository } from "../../../repositories";
3
3
  import { getStoreBySlug } from "./StoreDetailLayoutView";
4
4
  import { CategoryBundlesListing } from "../../categories/components/CategoryBundlesListing";
5
+ import { safeRead } from "../../../errors/safe-read";
5
6
  /**
6
7
  * Public store → Bundles tab.
7
8
  *
@@ -16,9 +17,12 @@ export async function StoreBundlesPageView({ storeSlug, onBuyNow, }) {
16
17
  if (!storeId) {
17
18
  return null;
18
19
  }
19
- const bundles = await categoriesRepository
20
+ const bundles = await safeRead(() => categoriesRepository
20
21
  .listByType("bundle", { activeOnly: true, limit: 50 })
21
- .then((rows) => rows.filter((c) => c.createdByStoreId === storeId))
22
- .catch(() => []);
22
+ .then((rows) => rows.filter((c) => c.createdByStoreId === storeId)), {
23
+ route: "/stores/[storeSlug]/bundles",
24
+ key: "categories.listByType(bundle)",
25
+ fallback: [],
26
+ });
23
27
  return _jsx(CategoryBundlesListing, { initialBundles: bundles, onBuyNow: onBuyNow });
24
28
  }
@@ -3,6 +3,7 @@ import { storeRepository } from "../../../repositories";
3
3
  import { Container, Heading, Main, Section } from "../../../ui";
4
4
  import { AdSlot } from "../../homepage/components/AdSlot";
5
5
  import { StoresIndexListing } from "./StoresIndexListing";
6
+ import { safeRead } from "../../../errors/safe-read";
6
7
  function sp(params, key) {
7
8
  const v = params[key];
8
9
  return Array.isArray(v) ? v[0] ?? "" : v ?? "";
@@ -11,8 +12,6 @@ export async function StoresIndexPageView({ searchParams = {} }) {
11
12
  const sort = sp(searchParams, "sort") || "-createdAt";
12
13
  const page = Number(sp(searchParams, "page")) || 1;
13
14
  const pageSize = Number(sp(searchParams, "pageSize")) || 24;
14
- const result = await storeRepository
15
- .listStores({ page, pageSize, sorts: sort }, true)
16
- .catch(() => null);
15
+ const result = await safeRead(() => storeRepository.listStores({ page, pageSize, sorts: sort }, true), { route: "/stores", key: "stores.listStores", fallback: null });
17
16
  return (_jsx(Main, { children: _jsx(Section, { padding: "y-2xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-8", color: "primary", size: "3xl", weight: "semibold", children: "Stores" }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(StoresIndexListing, { initialData: result ?? undefined }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
18
17
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.32.1",
3
+ "version": "4.32.3",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"