@mohasinac/appkit 4.11.1 → 4.11.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.
- package/dist/_internal/server/features/checkout/actions.d.ts +4 -0
- package/dist/_internal/server/features/checkout/actions.js +138 -27
- package/dist/_internal/server/features/checkout/data.d.ts +10 -0
- package/dist/_internal/server/features/checkout/locked-lines.d.ts +30 -0
- package/dist/_internal/server/features/checkout/locked-lines.js +116 -0
- package/dist/_internal/server/features/orders/adapters.js +7 -0
- package/dist/_internal/server/features/products/data.d.ts +12 -3
- package/dist/_internal/server/features/products/data.js +55 -4
- package/dist/_internal/server/features/products/index.d.ts +2 -1
- package/dist/_internal/server/features/products/index.js +2 -1
- package/dist/_internal/server/features/products/list-public.d.ts +130 -0
- package/dist/_internal/server/features/products/list-public.js +375 -0
- package/dist/_internal/server/features/reviews/data.d.ts +1 -1
- package/dist/_internal/server/features/reviews/data.js +3 -2
- package/dist/_internal/server/features/tester/visibility.js +3 -3
- package/dist/_internal/server/jobs/core/auctionSettlement.js +89 -16
- package/dist/_internal/server/jobs/core/offerExpiry.js +116 -4
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +1 -1
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +4 -1
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +2 -0
- package/dist/_internal/server/jobs/handlers/_helpers.js +4 -1
- package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -0
- package/dist/_internal/server/jobs/handlers/messages.js +5 -0
- package/dist/_internal/shared/checkout/lanes.d.ts +41 -0
- package/dist/_internal/shared/checkout/lanes.js +106 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +19 -0
- package/dist/_internal/shared/checkout/order-math.js +30 -6
- package/dist/_internal/shared/features/promotions/schema.d.ts +0 -16
- package/dist/_internal/shared/features/promotions/schema.js +0 -2
- package/dist/_internal/shared/features/reviews/config.d.ts +8 -1
- package/dist/_internal/shared/features/reviews/config.js +8 -1
- package/dist/_internal/shared/listing-types/_registry.d.ts +88 -1
- package/dist/_internal/shared/listing-types/_registry.js +63 -1
- package/dist/_internal/shared/listing-types/art/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/art/config.js +12 -0
- package/dist/_internal/shared/listing-types/auction/config.d.ts +53 -0
- package/dist/_internal/shared/listing-types/auction/config.js +17 -0
- package/dist/_internal/shared/listing-types/classified/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/classified/config.js +14 -0
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/digital-code/config.js +12 -0
- package/dist/_internal/shared/listing-types/feature-flags.d.ts +1 -0
- package/dist/_internal/shared/listing-types/feature-flags.js +24 -10
- package/dist/_internal/shared/listing-types/live/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/live/config.js +14 -0
- package/dist/_internal/shared/listing-types/pre-order/config.d.ts +110 -0
- package/dist/_internal/shared/listing-types/pre-order/config.js +14 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +44 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +10 -0
- package/dist/_internal/shared/listing-types/standard/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/standard/config.js +9 -0
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/stickers/config.js +12 -0
- package/dist/client.d.ts +7 -2
- package/dist/client.js +8 -2
- package/dist/constants/field-names.d.ts +12 -0
- package/dist/constants/field-names.js +12 -0
- package/dist/constants/table-keys.d.ts +2 -0
- package/dist/constants/table-keys.js +2 -0
- package/dist/features/account/components/NotificationBell.js +1 -0
- package/dist/features/account/components/UserOffersPanel.js +8 -1
- package/dist/features/account/components/UserOrdersView.js +27 -5
- package/dist/features/admin/actions/notification-actions.js +1 -1
- package/dist/features/admin/components/AdminAddressesView.js +3 -1
- package/dist/features/admin/components/AdminCouponEditorView.js +1 -4
- package/dist/features/admin/components/AdminPaymentMethodsView.js +3 -1
- package/dist/features/admin/components/AdminProductsView.js +34 -18
- package/dist/features/admin/constants/filter-tabs.d.ts +26 -9
- package/dist/features/admin/constants/filter-tabs.js +22 -9
- package/dist/features/admin/schemas/firestore.d.ts +2 -1
- package/dist/features/admin/schemas/firestore.js +1 -0
- package/dist/features/auctions/actions/bid-actions.d.ts +11 -1
- package/dist/features/auctions/actions/bid-actions.js +29 -15
- package/dist/features/auctions/components/AuctionBidsTable.js +6 -2
- package/dist/features/auctions/components/AuctionBottomActions.js +1 -0
- package/dist/features/auctions/components/AuctionDetailPageView.js +13 -11
- package/dist/features/auctions/components/AuctionsListView.js +12 -53
- package/dist/features/auctions/components/PlaceBidFormClient.js +10 -1
- package/dist/features/auctions/repository/bid.repository.d.ts +15 -0
- package/dist/features/auctions/repository/bid.repository.js +19 -0
- package/dist/features/auctions/schemas/firestore.d.ts +11 -1
- package/dist/features/auctions/schemas/firestore.js +1 -0
- package/dist/features/cart/actions/cart-actions.d.ts +11 -0
- package/dist/features/cart/actions/cart-actions.js +24 -0
- package/dist/features/cart/repository/cart.repository.d.ts +24 -1
- package/dist/features/cart/repository/cart.repository.js +71 -6
- package/dist/features/cart/schemas/firestore.d.ts +23 -4
- package/dist/features/categories/components/BrandDetailTabs.js +31 -11
- package/dist/features/categories/components/CategoryDetailTabs.js +27 -7
- package/dist/features/categories/components/CategoryProductsListing.d.ts +9 -1
- package/dist/features/categories/components/CategoryProductsListing.js +5 -2
- package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -1
- package/dist/features/contact/email.js +56 -1
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +17 -16
- package/dist/features/homepage/components/FeaturedResultsSection.js +1 -1
- package/dist/features/homepage/components/SectionCarousel.js +1 -1
- package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
- package/dist/features/layout/AppLayoutShell.js +9 -2
- package/dist/features/layout/BottomActions.js +26 -3
- package/dist/features/layout/BottomActionsContext.d.ts +14 -0
- package/dist/features/layout/BottomActionsContext.js +13 -1
- package/dist/features/layout/hooks/useBottomActions.d.ts +6 -1
- package/dist/features/layout/hooks/useBottomActions.js +4 -1
- package/dist/features/layout/index.d.ts +1 -1
- package/dist/features/live/components/LiveItemDetailPageView.js +7 -6
- package/dist/features/orders/repository/orders.repository.d.ts +4 -15
- package/dist/features/orders/repository/orders.repository.js +4 -27
- package/dist/features/orders/types/index.d.ts +16 -0
- package/dist/features/orders/utils/order-splitter.js +14 -2
- package/dist/features/pre-orders/components/PreOrderBottomActions.d.ts +1 -1
- package/dist/features/pre-orders/components/PreOrderBottomActions.js +3 -19
- package/dist/features/pre-orders/components/PreOrdersListView.js +11 -39
- package/dist/features/products/components/ArtStickersListView.js +10 -49
- package/dist/features/products/components/AuctionsIndexListing.js +1 -2
- package/dist/features/products/components/ListingBottomActions.d.ts +28 -0
- package/dist/features/products/components/ListingBottomActions.js +25 -0
- package/dist/features/products/components/PrizeDrawBottomActions.d.ts +1 -1
- package/dist/features/products/components/PrizeDrawBottomActions.js +3 -21
- package/dist/features/products/components/ProductDetailActions.js +4 -0
- package/dist/features/products/components/ProductDetailPageView.js +7 -7
- package/dist/features/products/components/ProductsIndexPageView.js +9 -62
- package/dist/features/products/constants/listing-tabs.d.ts +43 -198
- package/dist/features/products/constants/listing-tabs.js +71 -58
- package/dist/features/products/constants/sieve.d.ts +104 -1
- package/dist/features/products/constants/sieve.js +27 -7
- package/dist/features/products/repository/products.repository.d.ts +45 -1
- package/dist/features/products/repository/products.repository.js +67 -8
- package/dist/features/products/utils/listing-badge-variant.d.ts +20 -1
- package/dist/features/products/utils/listing-badge-variant.js +26 -1
- package/dist/features/products/utils/listing-type.d.ts +24 -0
- package/dist/features/products/utils/listing-type.js +48 -0
- package/dist/features/promotions/actions/coupon-actions.d.ts +7 -0
- package/dist/features/promotions/actions/coupon-actions.js +12 -1
- package/dist/features/promotions/actions/coupon-stacking.d.ts +24 -0
- package/dist/features/promotions/actions/coupon-stacking.js +21 -0
- package/dist/features/promotions/actions/index.d.ts +1 -0
- package/dist/features/promotions/actions/index.js +1 -0
- package/dist/features/promotions/actions/seller-coupon-actions.d.ts +0 -1
- package/dist/features/promotions/actions/seller-coupon-actions.js +0 -1
- package/dist/features/promotions/components/CouponHelpDetails.d.ts +20 -0
- package/dist/features/promotions/components/CouponHelpDetails.js +17 -0
- package/dist/features/promotions/components/CouponsIndexListing.js +2 -1
- package/dist/features/promotions/components/index.d.ts +2 -0
- package/dist/features/promotions/components/index.js +1 -0
- package/dist/features/promotions/constants/coupon-help.d.ts +27 -0
- package/dist/features/promotions/constants/coupon-help.js +42 -0
- package/dist/features/promotions/repository/coupons.repository.d.ts +10 -1
- package/dist/features/promotions/repository/coupons.repository.js +22 -8
- package/dist/features/promotions/schemas/firestore.d.ts +0 -1
- package/dist/features/promotions/schemas/firestore.js +1 -1
- package/dist/features/promotions/schemas/index.d.ts +0 -20
- package/dist/features/promotions/schemas/index.js +0 -1
- package/dist/features/reviews/actions/review-actions.js +5 -1
- package/dist/features/reviews/components/ReviewDetailPageView.js +2 -1
- package/dist/features/reviews/components/ReviewFilters.d.ts +7 -1
- package/dist/features/reviews/components/ReviewFilters.js +4 -3
- package/dist/features/reviews/components/ReviewsIndexListing.d.ts +6 -0
- package/dist/features/reviews/components/ReviewsIndexListing.js +9 -109
- package/dist/features/reviews/components/ReviewsListingPanel.d.ts +41 -0
- package/dist/features/reviews/components/ReviewsListingPanel.js +226 -0
- package/dist/features/reviews/components/index.d.ts +2 -0
- package/dist/features/reviews/components/index.js +1 -0
- package/dist/features/reviews/hooks/useReviews.d.ts +8 -0
- package/dist/features/reviews/hooks/useReviews.js +14 -6
- package/dist/features/reviews/repository/reviews.repository.d.ts +29 -1
- package/dist/features/reviews/repository/reviews.repository.js +77 -41
- package/dist/features/reviews/types/index.d.ts +8 -1
- package/dist/features/seller/actions/offer-actions.d.ts +6 -0
- package/dist/features/seller/actions/offer-actions.js +8 -9
- package/dist/features/seller/components/SellerOffersView.d.ts +6 -1
- package/dist/features/seller/components/SellerOffersView.js +59 -8
- package/dist/features/seller/components/SellerProductsCards.js +2 -2
- package/dist/features/seller/components/SellerProductsView.js +21 -18
- package/dist/features/seller/repository/offer.repository.d.ts +18 -2
- package/dist/features/seller/repository/offer.repository.js +38 -2
- package/dist/features/seller/schemas/firestore.d.ts +7 -2
- package/dist/features/seller/schemas/firestore.js +3 -0
- package/dist/features/seller/schemas/offer-forms.d.ts +12 -0
- package/dist/features/seller/schemas/offer-forms.js +28 -0
- package/dist/features/stores/actions/store-query-actions.js +1 -1
- package/dist/features/stores/components/StoreAuctionsListing.js +5 -11
- package/dist/features/stores/components/StorePreOrdersListing.js +6 -11
- package/dist/features/stores/components/StoreReviewsListing.d.ts +4 -0
- package/dist/features/stores/components/StoreReviewsListing.js +7 -108
- package/dist/features/stores/schemas/index.d.ts +2 -2
- package/dist/features/tester/seed-data/coupons-tester-seed-data.js +3 -3
- package/dist/features/tester/seed-data/index.d.ts +1 -0
- package/dist/features/tester/seed-data/index.js +1 -0
- package/dist/features/tester/seed-data/offers-tester-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/offers-tester-seed-data.js +61 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +337 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.js +15 -3
- package/dist/react/hooks/useListingTypeFlags.d.ts +12 -19
- package/dist/react/hooks/useListingTypeFlags.js +4 -9
- package/dist/schemas/registry.d.ts +0 -12
- package/dist/seed/claimed-coupons-seed-data.js +3 -3
- package/dist/seed/coupons-seed-data.js +29 -10
- package/dist/seed/faq-seed-data.js +2 -2
- package/dist/seed/manifest.js +2 -2
- package/dist/server-entry.d.ts +1 -0
- package/dist/server-entry.js +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +3 -0
- package/dist/styles.css +44 -39
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/FilterChipGroup.d.ts +21 -3
- package/dist/ui/components/FilterChipGroup.js +33 -4
- package/dist/ui/components/HorizontalScroller.d.ts +1 -2
- package/dist/ui/components/HorizontalScroller.js +9 -5
- package/dist/ui/components/HorizontalScroller.style.css +43 -38
- package/package.json +1 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { JsonValue } from "../../../../schemas/types";
|
|
2
|
+
/** Vercel Hobby Fluid Compute ceiling (CLAUDE.md Rule #6) — never fetch more at once. */
|
|
3
|
+
export declare const PUBLIC_PRODUCT_MAX_PAGE_SIZE = 50;
|
|
4
|
+
/**
|
|
5
|
+
* Which "Show X" defaults apply to a set of listing types.
|
|
6
|
+
*
|
|
7
|
+
* THE POINT OF THIS FUNCTION is that SSR and the client refetch call the SAME
|
|
8
|
+
* implementation. `staleTime: Infinity` freezes SSR `initialData` forever, so
|
|
9
|
+
* if the SSR filter-builder computes a different default than the client's
|
|
10
|
+
* bare-URL state, the first paint and every later refetch disagree and one of
|
|
11
|
+
* them is wrong — permanently (Root Cause #30). Two mirrored literals is
|
|
12
|
+
* exactly how that bug is written; one shared function is how it isn't.
|
|
13
|
+
*
|
|
14
|
+
* `/products` now spans all nine listing types, which makes this load-bearing
|
|
15
|
+
* rather than theoretical: a mixed set includes both sold-out-able types
|
|
16
|
+
* (products, art) and time-boxed ones (auctions, pre-orders, prize draws), so
|
|
17
|
+
* BOTH toggles can apply at once.
|
|
18
|
+
*/
|
|
19
|
+
export declare function defaultTogglesForListingTypes(types: readonly string[] | undefined): {
|
|
20
|
+
hideSoldByDefault: boolean;
|
|
21
|
+
hideEndedByDefault: boolean;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* A product document as it comes back from either executor. Deliberately
|
|
25
|
+
* `JsonValue`-shaped rather than `ProductDocument`: the upstream listingProcessor
|
|
26
|
+
* Function returns already-serialised rows, and this module only ever reads a
|
|
27
|
+
* handful of scalar fields off them (stockQuantity, listingType, the sort key).
|
|
28
|
+
*/
|
|
29
|
+
type Row = Record<string, JsonValue>;
|
|
30
|
+
export interface PublicProductListInput {
|
|
31
|
+
/**
|
|
32
|
+
* Listing types this page spans. Emitted as one pipe-joined OR-group, which
|
|
33
|
+
* the Firebase Sieve adapter upgrades to a `.where(…, "in", …)` query.
|
|
34
|
+
*/
|
|
35
|
+
listingTypes?: readonly string[];
|
|
36
|
+
q?: string;
|
|
37
|
+
category?: string;
|
|
38
|
+
brand?: string;
|
|
39
|
+
/** Pipe-joined multi-select (`new|used`) — emitted as an OR-group, never as AND. */
|
|
40
|
+
condition?: string;
|
|
41
|
+
storeId?: string;
|
|
42
|
+
status?: string;
|
|
43
|
+
minPrice?: string;
|
|
44
|
+
maxPrice?: string;
|
|
45
|
+
minBid?: string;
|
|
46
|
+
maxBid?: string;
|
|
47
|
+
featured?: boolean;
|
|
48
|
+
isPromoted?: boolean;
|
|
49
|
+
freeShipping?: boolean;
|
|
50
|
+
isPartOfBundle?: boolean;
|
|
51
|
+
features?: readonly string[];
|
|
52
|
+
preOrderProductionStatus?: string;
|
|
53
|
+
prizeRevealStatus?: string;
|
|
54
|
+
/** Hide sold-out rows. Applied in memory — never pushed into Firestore. */
|
|
55
|
+
inStock?: boolean;
|
|
56
|
+
dateFrom?: string;
|
|
57
|
+
dateTo?: string;
|
|
58
|
+
/** Pre-validated raw Sieve string (callers must safelist fields themselves). */
|
|
59
|
+
rawFilters?: string | null;
|
|
60
|
+
page?: number;
|
|
61
|
+
pageSize?: number;
|
|
62
|
+
sorts?: string;
|
|
63
|
+
cursor?: string | null;
|
|
64
|
+
}
|
|
65
|
+
export interface PublicProductListResult {
|
|
66
|
+
items: Row[];
|
|
67
|
+
total: number;
|
|
68
|
+
page: number;
|
|
69
|
+
pageSize: number;
|
|
70
|
+
totalPages: number;
|
|
71
|
+
hasMore: boolean;
|
|
72
|
+
cursor: string | null;
|
|
73
|
+
/** The Sieve string actually sent to Firestore — surfaced for debugging. */
|
|
74
|
+
filters: string;
|
|
75
|
+
sorts: string;
|
|
76
|
+
}
|
|
77
|
+
export interface PublicProductQuery {
|
|
78
|
+
filters: string;
|
|
79
|
+
sorts: string;
|
|
80
|
+
page: number;
|
|
81
|
+
pageSize: number;
|
|
82
|
+
cursor?: string | null;
|
|
83
|
+
}
|
|
84
|
+
export interface ExecutorResult {
|
|
85
|
+
items: unknown[];
|
|
86
|
+
total: number;
|
|
87
|
+
page: number;
|
|
88
|
+
totalPages: number;
|
|
89
|
+
hasMore: boolean;
|
|
90
|
+
cursor?: string | null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* How the query actually runs. Defaults to `productRepository.list`. The consumer
|
|
94
|
+
* route overrides this to prefer the colocated `listingProcessor` Function and fall
|
|
95
|
+
* back to the repository — the seam exists so that preference never has to be
|
|
96
|
+
* duplicated into appkit, which has no access to the consumer's env.
|
|
97
|
+
*/
|
|
98
|
+
export type PublicProductExecutor = (query: PublicProductQuery) => Promise<ExecutorResult>;
|
|
99
|
+
export interface PublicProductListOptions {
|
|
100
|
+
executor?: PublicProductExecutor;
|
|
101
|
+
/**
|
|
102
|
+
* When supplied, rows whose `listingType` is not in this set are stripped after
|
|
103
|
+
* the fetch. Callers that can read siteSettings pass `enabledListingTypes(...)`.
|
|
104
|
+
*/
|
|
105
|
+
enabledListingTypes?: ReadonlySet<string>;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Read the shared public-listing query params. SSR views hand in Next's
|
|
109
|
+
* `searchParams` record; the API route hands in `url.searchParams`. Both produce
|
|
110
|
+
* the same typed input, which is the whole point — the two paths cannot drift.
|
|
111
|
+
*/
|
|
112
|
+
export declare function parsePublicProductParams(params: URLSearchParams | Record<string, string | string[] | undefined>, defaults?: {
|
|
113
|
+
listingTypes?: readonly string[];
|
|
114
|
+
pageSize?: number;
|
|
115
|
+
sorts?: string;
|
|
116
|
+
/**
|
|
117
|
+
* Public browse pages hide sold-out rows until the user flips "Show sold".
|
|
118
|
+
* The API route leaves this off so non-browse callers (related items,
|
|
119
|
+
* homepage sections) keep their current behaviour.
|
|
120
|
+
*/
|
|
121
|
+
hideSoldByDefault?: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Auctions / pre-orders hide already-ended rows until "Show ended" is on,
|
|
124
|
+
* by defaulting `dateFrom` to now. Must mirror the paired client listing
|
|
125
|
+
* component exactly or the SSR paint and the refetch disagree.
|
|
126
|
+
*/
|
|
127
|
+
hideEndedByDefault?: boolean;
|
|
128
|
+
}): PublicProductListInput;
|
|
129
|
+
export declare function listPublicProducts(input: PublicProductListInput, opts?: PublicProductListOptions): Promise<PublicProductListResult | null>;
|
|
130
|
+
export {};
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: One implementation of "list products for a public listing page", shared by
|
|
3
|
+
* every SSR listing view AND `/api/products`. Before 2026-08-21 there were five
|
|
4
|
+
* hand-rolled copies of this filter logic (four SSR views + the route) and they
|
|
5
|
+
* had drifted: the route had learned — via `6fe4e0dd8` and `efb7d1b6a` — that an
|
|
6
|
+
* `inStock` (`stockQuantity>0`) or mismatched date-range inequality must NEVER be
|
|
7
|
+
* pushed into the Firestore query, while the SSR views still pushed it and
|
|
8
|
+
* swallowed the resulting FAILED_PRECONDITION as "no results". That is what made
|
|
9
|
+
* /art render empty until "Show sold" was clicked.
|
|
10
|
+
*
|
|
11
|
+
* WHAT: `parsePublicProductParams` (URL/searchParams -> typed input) and
|
|
12
|
+
* `listPublicProducts` (typed input -> page of documents). Firestore-safe
|
|
13
|
+
* clauses go into the query; `inStock` and unsafe date ranges are applied as
|
|
14
|
+
* in-memory predicates over one bounded fetch, then re-sorted into the caller's
|
|
15
|
+
* requested order and re-paginated.
|
|
16
|
+
*
|
|
17
|
+
* EXPORTS:
|
|
18
|
+
* PublicProductListInput, PublicProductListResult, PublicProductExecutor,
|
|
19
|
+
* PublicProductListOptions, parsePublicProductParams, listPublicProducts
|
|
20
|
+
*
|
|
21
|
+
* @tag domain:products
|
|
22
|
+
* @tag layer:server-data
|
|
23
|
+
* @tag pattern:none
|
|
24
|
+
* @tag access:server-only
|
|
25
|
+
* @tag consumers:ProductsIndexPageView,ArtStickersListView,AuctionsListView,PreOrdersListView,api/products
|
|
26
|
+
* @tag sideEffects:firestore-read
|
|
27
|
+
*/
|
|
28
|
+
import { productRepository } from "../../../../repositories";
|
|
29
|
+
import { normalizeError } from "../../../../errors/normalize";
|
|
30
|
+
import { serverLogger } from "../../../../monitoring/server-logger";
|
|
31
|
+
import { PRODUCT_FIELDS } from "../../../../constants/field-names";
|
|
32
|
+
import { TABLE_KEYS } from "../../../../constants/table-keys";
|
|
33
|
+
import { sortBy } from "../../../../constants/sort";
|
|
34
|
+
import { SIEVE_OP, sieveAnd, sieveFilter, expandSieveParam, } from "../../../../utils/sieve-builder";
|
|
35
|
+
import { ALL_LISTING_TYPES } from "../../../shared/listing-types/feature-flags";
|
|
36
|
+
import { hideDefaultsFor } from "../../../shared/listing-types/_registry";
|
|
37
|
+
/** Vercel Hobby Fluid Compute ceiling (CLAUDE.md Rule #6) — never fetch more at once. */
|
|
38
|
+
export const PUBLIC_PRODUCT_MAX_PAGE_SIZE = 50;
|
|
39
|
+
/**
|
|
40
|
+
* Which "Show X" defaults apply to a set of listing types.
|
|
41
|
+
*
|
|
42
|
+
* THE POINT OF THIS FUNCTION is that SSR and the client refetch call the SAME
|
|
43
|
+
* implementation. `staleTime: Infinity` freezes SSR `initialData` forever, so
|
|
44
|
+
* if the SSR filter-builder computes a different default than the client's
|
|
45
|
+
* bare-URL state, the first paint and every later refetch disagree and one of
|
|
46
|
+
* them is wrong — permanently (Root Cause #30). Two mirrored literals is
|
|
47
|
+
* exactly how that bug is written; one shared function is how it isn't.
|
|
48
|
+
*
|
|
49
|
+
* `/products` now spans all nine listing types, which makes this load-bearing
|
|
50
|
+
* rather than theoretical: a mixed set includes both sold-out-able types
|
|
51
|
+
* (products, art) and time-boxed ones (auctions, pre-orders, prize draws), so
|
|
52
|
+
* BOTH toggles can apply at once.
|
|
53
|
+
*/
|
|
54
|
+
export function defaultTogglesForListingTypes(types) {
|
|
55
|
+
// No explicit set = every type is in play, so both defaults apply.
|
|
56
|
+
const effective = types && types.length > 0 ? types : ALL_LISTING_TYPES;
|
|
57
|
+
const defaults = hideDefaultsFor(effective.filter((t) => ALL_LISTING_TYPES.includes(t)));
|
|
58
|
+
return {
|
|
59
|
+
hideSoldByDefault: defaults.includes("sold"),
|
|
60
|
+
// "ended" and "closed" are the same query mechanic (a date range against
|
|
61
|
+
// the type's own end field); they differ only in the toggle's wording.
|
|
62
|
+
hideEndedByDefault: defaults.includes("ended") || defaults.includes("closed"),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const DEFAULT_PAGE = 1;
|
|
66
|
+
const DEFAULT_PAGE_SIZE = 24;
|
|
67
|
+
const DEFAULT_SORTS = sortBy(PRODUCT_FIELDS.CREATED_AT);
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Param parsing
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
function first(params, key) {
|
|
72
|
+
if (params instanceof URLSearchParams)
|
|
73
|
+
return params.get(key) ?? "";
|
|
74
|
+
const v = params[key];
|
|
75
|
+
return (Array.isArray(v) ? v[0] : v) ?? "";
|
|
76
|
+
}
|
|
77
|
+
function num(raw) {
|
|
78
|
+
if (!raw)
|
|
79
|
+
return undefined;
|
|
80
|
+
const n = Number(raw);
|
|
81
|
+
return Number.isFinite(n) ? n : undefined;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Read the shared public-listing query params. SSR views hand in Next's
|
|
85
|
+
* `searchParams` record; the API route hands in `url.searchParams`. Both produce
|
|
86
|
+
* the same typed input, which is the whole point — the two paths cannot drift.
|
|
87
|
+
*/
|
|
88
|
+
export function parsePublicProductParams(params, defaults) {
|
|
89
|
+
const get = (k) => first(params, k);
|
|
90
|
+
// The type filter is a multi-select checkbox group, so its URL value is a
|
|
91
|
+
// pipe-joined set (`auction|pre-order`). Selected types NARROW the page's
|
|
92
|
+
// span; anything outside the span is discarded rather than allowed to widen
|
|
93
|
+
// it (an /art URL must not be able to ask for auctions). Nothing selected —
|
|
94
|
+
// or nothing valid selected — means the page's full span.
|
|
95
|
+
const typeParam = get(TABLE_KEYS.LISTING_TYPE);
|
|
96
|
+
const span = defaults?.listingTypes;
|
|
97
|
+
const selected = typeParam
|
|
98
|
+
.split("|")
|
|
99
|
+
.map((t) => t.trim())
|
|
100
|
+
.filter((t) => t && t !== "All");
|
|
101
|
+
const narrowed = span ? selected.filter((t) => span.includes(t)) : selected;
|
|
102
|
+
const listingTypes = narrowed.length > 0 ? narrowed : span;
|
|
103
|
+
const featuresRaw = get(TABLE_KEYS.FEATURES);
|
|
104
|
+
return {
|
|
105
|
+
listingTypes,
|
|
106
|
+
q: get(TABLE_KEYS.QUERY) || undefined,
|
|
107
|
+
category: get(TABLE_KEYS.CATEGORY) || get(TABLE_KEYS.CATEGORY_SLUG) || undefined,
|
|
108
|
+
brand: get(TABLE_KEYS.BRAND) || undefined,
|
|
109
|
+
condition: get(TABLE_KEYS.CONDITION) || undefined,
|
|
110
|
+
storeId: get(TABLE_KEYS.STORE_ID) || get(TABLE_KEYS.SELLER) || undefined,
|
|
111
|
+
status: get(TABLE_KEYS.STATUS) || undefined,
|
|
112
|
+
minPrice: get(TABLE_KEYS.MIN_PRICE) || undefined,
|
|
113
|
+
maxPrice: get(TABLE_KEYS.MAX_PRICE) || undefined,
|
|
114
|
+
minBid: get(TABLE_KEYS.MIN_BID) || undefined,
|
|
115
|
+
maxBid: get(TABLE_KEYS.MAX_BID) || undefined,
|
|
116
|
+
featured: get(TABLE_KEYS.FEATURED) === "true" || undefined,
|
|
117
|
+
isPromoted: get("isPromoted") === "true" || undefined,
|
|
118
|
+
freeShipping: get(TABLE_KEYS.FREE_SHIPPING) === "true" || undefined,
|
|
119
|
+
isPartOfBundle: get(TABLE_KEYS.IS_PART_OF_BUNDLE) === "true" || undefined,
|
|
120
|
+
features: featuresRaw ? featuresRaw.split("|").filter(Boolean) : undefined,
|
|
121
|
+
preOrderProductionStatus: get(TABLE_KEYS.PREORDER_STATUS) || get("preOrderStatus") || undefined,
|
|
122
|
+
prizeRevealStatus: get(TABLE_KEYS.PRIZE_REVEAL_STATUS) || undefined,
|
|
123
|
+
// Three spellings of one intent — /products and /art call it "Show sold",
|
|
124
|
+
// /pre-orders calls it "Show closed", and the client hook sends `inStock`.
|
|
125
|
+
// Any of them turning the filter OFF wins, so SSR and the client refetch
|
|
126
|
+
// agree on what the default view means (Root Cause #30).
|
|
127
|
+
inStock: get(TABLE_KEYS.SHOW_SOLD) === "true" || get(TABLE_KEYS.SHOW_CLOSED) === "true"
|
|
128
|
+
? undefined
|
|
129
|
+
: get(TABLE_KEYS.IN_STOCK)
|
|
130
|
+
? get(TABLE_KEYS.IN_STOCK) === "true"
|
|
131
|
+
: defaults?.hideSoldByDefault
|
|
132
|
+
? true
|
|
133
|
+
: undefined,
|
|
134
|
+
dateFrom: get(TABLE_KEYS.DATE_FROM) ||
|
|
135
|
+
(defaults?.hideEndedByDefault && get(TABLE_KEYS.SHOW_ENDED) !== "true"
|
|
136
|
+
? new Date().toISOString()
|
|
137
|
+
: undefined),
|
|
138
|
+
dateTo: get(TABLE_KEYS.DATE_TO) || undefined,
|
|
139
|
+
page: num(get(TABLE_KEYS.PAGE)) ?? DEFAULT_PAGE,
|
|
140
|
+
pageSize: num(get(TABLE_KEYS.PAGE_SIZE)) ?? defaults?.pageSize ?? DEFAULT_PAGE_SIZE,
|
|
141
|
+
sorts: get(TABLE_KEYS.SORT) || defaults?.sorts || DEFAULT_SORTS,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Filter building
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
/**
|
|
148
|
+
* Only clauses Firestore can satisfy alongside an arbitrary `orderBy`. Notably
|
|
149
|
+
* ABSENT: `inStock` (a `stockQuantity` range) and, unless the sort matches, the
|
|
150
|
+
* auction/pre-order date ranges. Firestore appends an inequality field to the
|
|
151
|
+
* orderBy implicitly, so pairing one with an unrelated sort demands a composite
|
|
152
|
+
* index in an order nobody declares — it fails with FAILED_PRECONDITION, which
|
|
153
|
+
* upstream reads as a bare "no results".
|
|
154
|
+
*/
|
|
155
|
+
function buildFirestoreSafeFilters(input) {
|
|
156
|
+
const parts = [];
|
|
157
|
+
// Published-only unless the caller explicitly asks otherwise. Without this
|
|
158
|
+
// default, any refetch that omits `status` leaks drafts (Root Cause #30).
|
|
159
|
+
parts.push(sieveFilter(PRODUCT_FIELDS.STATUS, SIEVE_OP.EQ, input.status || PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED));
|
|
160
|
+
const types = input.listingTypes?.filter(Boolean) ?? [];
|
|
161
|
+
if (types.length > 0) {
|
|
162
|
+
parts.push(sieveFilter(PRODUCT_FIELDS.LISTING_TYPE, SIEVE_OP.EQ, types.join("|")));
|
|
163
|
+
}
|
|
164
|
+
if (input.category) {
|
|
165
|
+
parts.push(expandSieveParam(PRODUCT_FIELDS.CATEGORY_SLUGS, input.category, SIEVE_OP.CONTAINS));
|
|
166
|
+
}
|
|
167
|
+
if (input.brand)
|
|
168
|
+
parts.push(sieveFilter(PRODUCT_FIELDS.BRAND, SIEVE_OP.EQ, input.brand));
|
|
169
|
+
if (input.condition) {
|
|
170
|
+
// Pipe-joined OR-group, NOT `sieveMultiEq` — that emits
|
|
171
|
+
// `condition==new,condition==used`, an AND of two equalities on one field,
|
|
172
|
+
// which can never match a document.
|
|
173
|
+
const values = input.condition.split("|").filter(Boolean);
|
|
174
|
+
if (values.length > 0) {
|
|
175
|
+
parts.push(sieveFilter(PRODUCT_FIELDS.CONDITION, SIEVE_OP.EQ, values.join("|")));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (input.storeId)
|
|
179
|
+
parts.push(sieveFilter(PRODUCT_FIELDS.STORE_ID, SIEVE_OP.EQ, input.storeId));
|
|
180
|
+
const minPrice = num(input.minPrice ?? "");
|
|
181
|
+
if (minPrice !== undefined)
|
|
182
|
+
parts.push(sieveFilter(PRODUCT_FIELDS.PRICE, SIEVE_OP.GTE, minPrice));
|
|
183
|
+
const maxPrice = num(input.maxPrice ?? "");
|
|
184
|
+
if (maxPrice !== undefined)
|
|
185
|
+
parts.push(sieveFilter(PRODUCT_FIELDS.PRICE, SIEVE_OP.LTE, maxPrice));
|
|
186
|
+
const minBid = num(input.minBid ?? "");
|
|
187
|
+
if (minBid !== undefined)
|
|
188
|
+
parts.push(sieveFilter(PRODUCT_FIELDS.CURRENT_BID, SIEVE_OP.GTE, minBid));
|
|
189
|
+
const maxBid = num(input.maxBid ?? "");
|
|
190
|
+
if (maxBid !== undefined)
|
|
191
|
+
parts.push(sieveFilter(PRODUCT_FIELDS.CURRENT_BID, SIEVE_OP.LTE, maxBid));
|
|
192
|
+
if (input.featured)
|
|
193
|
+
parts.push(sieveFilter(PRODUCT_FIELDS.FEATURED, SIEVE_OP.EQ, true));
|
|
194
|
+
if (input.isPromoted)
|
|
195
|
+
parts.push(sieveFilter(PRODUCT_FIELDS.IS_PROMOTED, SIEVE_OP.EQ, true));
|
|
196
|
+
if (input.freeShipping) {
|
|
197
|
+
parts.push(sieveFilter(PRODUCT_FIELDS.SHIPPING_PAID_BY, SIEVE_OP.EQ, PRODUCT_FIELDS.SHIPPING_PAID_BY_VALUES.SELLER));
|
|
198
|
+
}
|
|
199
|
+
if (input.isPartOfBundle)
|
|
200
|
+
parts.push(sieveFilter("isPartOfBundle", SIEVE_OP.EQ, true));
|
|
201
|
+
if (input.preOrderProductionStatus) {
|
|
202
|
+
parts.push(sieveFilter(PRODUCT_FIELDS.PRE_ORDER_PRODUCTION_STATUS, SIEVE_OP.EQ, input.preOrderProductionStatus));
|
|
203
|
+
}
|
|
204
|
+
if (input.prizeRevealStatus) {
|
|
205
|
+
parts.push(sieveFilter(PRODUCT_FIELDS.PRIZE_REVEAL_STATUS, SIEVE_OP.EQ, input.prizeRevealStatus));
|
|
206
|
+
}
|
|
207
|
+
if (input.q)
|
|
208
|
+
parts.push(sieveFilter(PRODUCT_FIELDS.TITLE, SIEVE_OP.CONTAINS_CI, input.q));
|
|
209
|
+
// `array-contains-any` isn't supported by the Sieve Firebase adapter, so only a
|
|
210
|
+
// single feature can be pushed down; multi-select falls through to the caller.
|
|
211
|
+
if (input.features?.length === 1) {
|
|
212
|
+
parts.push(sieveFilter(PRODUCT_FIELDS.FEATURES, SIEVE_OP.CONTAINS, input.features[0]));
|
|
213
|
+
}
|
|
214
|
+
if (input.rawFilters)
|
|
215
|
+
parts.push(input.rawFilters);
|
|
216
|
+
return sieveAnd(...parts.filter(Boolean));
|
|
217
|
+
}
|
|
218
|
+
/** Which Timestamp field a date range refers to, given the requested listing types. */
|
|
219
|
+
function dateFieldFor(types) {
|
|
220
|
+
if (!types || types.length !== 1)
|
|
221
|
+
return null;
|
|
222
|
+
if (types[0] === PRODUCT_FIELDS.LISTING_TYPE_VALUES.AUCTION) {
|
|
223
|
+
return PRODUCT_FIELDS.AUCTION_END_DATE;
|
|
224
|
+
}
|
|
225
|
+
if (types[0] === PRODUCT_FIELDS.LISTING_TYPE_VALUES.PRE_ORDER) {
|
|
226
|
+
return PRODUCT_FIELDS.PRE_ORDER_DELIVERY_DATE;
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
async function defaultExecutor(query) {
|
|
231
|
+
const result = await productRepository.list({
|
|
232
|
+
filters: query.filters,
|
|
233
|
+
sorts: query.sorts,
|
|
234
|
+
page: query.page,
|
|
235
|
+
pageSize: query.pageSize,
|
|
236
|
+
});
|
|
237
|
+
return {
|
|
238
|
+
items: result.items,
|
|
239
|
+
total: result.total,
|
|
240
|
+
page: result.page,
|
|
241
|
+
totalPages: result.totalPages,
|
|
242
|
+
hasMore: result.hasMore,
|
|
243
|
+
cursor: null,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// The one public listing query
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
export async function listPublicProducts(input, opts) {
|
|
250
|
+
const page = Math.max(1, input.page ?? DEFAULT_PAGE);
|
|
251
|
+
const pageSize = Math.min(PUBLIC_PRODUCT_MAX_PAGE_SIZE, Math.max(1, input.pageSize ?? DEFAULT_PAGE_SIZE));
|
|
252
|
+
const sorts = input.sorts || DEFAULT_SORTS;
|
|
253
|
+
const requestedSortField = sorts.replace(/^-/, "");
|
|
254
|
+
const dateField = dateFieldFor(input.listingTypes);
|
|
255
|
+
const hasDateRange = Boolean((input.dateFrom || input.dateTo) && dateField);
|
|
256
|
+
// Firestore DOES accept one inequality when the query sorts by that same field —
|
|
257
|
+
// the auctions "Ending Soon" default is exactly that shape, so push it down.
|
|
258
|
+
const canPushDate = hasDateRange && !input.inStock && requestedSortField === dateField;
|
|
259
|
+
const safeFilters = buildFirestoreSafeFilters(input);
|
|
260
|
+
const filters = sieveAnd(safeFilters, ...(canPushDate && input.dateFrom
|
|
261
|
+
? [sieveFilter(dateField, SIEVE_OP.GTE, input.dateFrom)]
|
|
262
|
+
: []), ...(canPushDate && input.dateTo
|
|
263
|
+
? [sieveFilter(dateField, SIEVE_OP.LTE, input.dateTo)]
|
|
264
|
+
: []));
|
|
265
|
+
const hasUnsafeFilter = Boolean(input.inStock) || (hasDateRange && !canPushDate);
|
|
266
|
+
// When a date range stays in memory, fetch in whichever direction front-loads
|
|
267
|
+
// the rows that will PASS it: `dateFrom` (">=", still live) wants the most
|
|
268
|
+
// future dates first; `dateTo` ("<=", ends by) wants the earliest first.
|
|
269
|
+
// Fetching in the client's own order instead is what made live auctions
|
|
270
|
+
// invisible once ~50 had already ended.
|
|
271
|
+
const fetchSorts = hasUnsafeFilter && hasDateRange && dateField
|
|
272
|
+
? input.dateFrom
|
|
273
|
+
? sortBy(dateField, "DESC")
|
|
274
|
+
: sortBy(dateField, "ASC")
|
|
275
|
+
: sorts;
|
|
276
|
+
const executor = opts?.executor ?? defaultExecutor;
|
|
277
|
+
let result;
|
|
278
|
+
try {
|
|
279
|
+
result = await executor({
|
|
280
|
+
filters,
|
|
281
|
+
sorts: fetchSorts,
|
|
282
|
+
page: hasUnsafeFilter ? 1 : page,
|
|
283
|
+
pageSize: hasUnsafeFilter ? PUBLIC_PRODUCT_MAX_PAGE_SIZE : pageSize,
|
|
284
|
+
cursor: hasUnsafeFilter ? null : (input.cursor ?? null),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
void normalizeError(error);
|
|
289
|
+
// Loud. A swallowed query failure here is indistinguishable from an empty
|
|
290
|
+
// catalogue at every call site above, which is exactly how the /art bug hid.
|
|
291
|
+
serverLogger.error("listPublicProducts query failed", {
|
|
292
|
+
filters,
|
|
293
|
+
sorts: fetchSorts,
|
|
294
|
+
error: error instanceof Error ? error.message : String(error),
|
|
295
|
+
});
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
let items = result.items;
|
|
299
|
+
let total = result.total;
|
|
300
|
+
let totalPages = result.totalPages;
|
|
301
|
+
let resultPage = result.page;
|
|
302
|
+
let hasMore = result.hasMore;
|
|
303
|
+
let cursor = result.cursor ?? null;
|
|
304
|
+
if (hasUnsafeFilter) {
|
|
305
|
+
const filtered = items.filter((item) => {
|
|
306
|
+
if (input.inStock) {
|
|
307
|
+
const stock = item[PRODUCT_FIELDS.STOCK_QUANTITY];
|
|
308
|
+
if (!(typeof stock === "number" && stock > 0))
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
if (hasDateRange && dateField) {
|
|
312
|
+
const raw = item[dateField];
|
|
313
|
+
if (!raw)
|
|
314
|
+
return false;
|
|
315
|
+
const value = raw instanceof Date ? raw.toISOString() : String(raw);
|
|
316
|
+
if (input.dateFrom && value < input.dateFrom)
|
|
317
|
+
return false;
|
|
318
|
+
if (input.dateTo && value > input.dateTo)
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
return true;
|
|
322
|
+
});
|
|
323
|
+
const ordered = fetchSorts === sorts ? filtered : sortRows(filtered, requestedSortField, sorts.startsWith("-"));
|
|
324
|
+
total = ordered.length;
|
|
325
|
+
totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
326
|
+
resultPage = page;
|
|
327
|
+
const start = (page - 1) * pageSize;
|
|
328
|
+
items = ordered.slice(start, start + pageSize);
|
|
329
|
+
hasMore = start + pageSize < total;
|
|
330
|
+
cursor = null;
|
|
331
|
+
}
|
|
332
|
+
// Feature-flagged-off types never reach a public surface.
|
|
333
|
+
const enabled = opts?.enabledListingTypes;
|
|
334
|
+
if (enabled && enabled.size > 0) {
|
|
335
|
+
const before = items.length;
|
|
336
|
+
items = items.filter((it) => {
|
|
337
|
+
const lt = typeof it.listingType === "string" ? it.listingType : "standard";
|
|
338
|
+
return enabled.has(lt);
|
|
339
|
+
});
|
|
340
|
+
const removed = before - items.length;
|
|
341
|
+
if (removed > 0)
|
|
342
|
+
total = Math.max(0, total - removed);
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
items,
|
|
346
|
+
total,
|
|
347
|
+
page: resultPage,
|
|
348
|
+
pageSize,
|
|
349
|
+
totalPages,
|
|
350
|
+
hasMore,
|
|
351
|
+
cursor,
|
|
352
|
+
filters,
|
|
353
|
+
sorts,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function sortRows(rows, field, desc) {
|
|
357
|
+
return [...rows].sort((a, b) => {
|
|
358
|
+
const av = a[field];
|
|
359
|
+
const bv = b[field];
|
|
360
|
+
if (av == null && bv == null)
|
|
361
|
+
return 0;
|
|
362
|
+
if (av == null)
|
|
363
|
+
return 1;
|
|
364
|
+
if (bv == null)
|
|
365
|
+
return -1;
|
|
366
|
+
const cmp = typeof av === "number" && typeof bv === "number"
|
|
367
|
+
? av - bv
|
|
368
|
+
: String(av) < String(bv)
|
|
369
|
+
? -1
|
|
370
|
+
: String(av) > String(bv)
|
|
371
|
+
? 1
|
|
372
|
+
: 0;
|
|
373
|
+
return desc ? -cmp : cmp;
|
|
374
|
+
});
|
|
375
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const getReviewsForProduct: (productId: string) => Promise<never[] | import("../../../..").ReviewDocument[]>;
|
|
1
|
+
export declare const getReviewsForProduct: (productId: string, limit?: number) => Promise<never[] | import("../../../..").ReviewDocument[]>;
|
|
2
2
|
export declare const getReviewsForStore: (storeId: string, page?: number, pageSize?: number) => Promise<import("../../../..").FirebaseSieveResult<import("../../../..").ReviewDocument> | {
|
|
3
3
|
items: never[];
|
|
4
4
|
total: number;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { cache } from "react";
|
|
2
2
|
import { reviewRepository, orderRepository } from "../../../../repositories";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import { REVIEWS_DETAIL_PAGE_SIZE } from "../../../shared/features/reviews/config";
|
|
4
|
+
export const getReviewsForProduct = cache(async (productId, limit = REVIEWS_DETAIL_PAGE_SIZE) => {
|
|
5
|
+
return reviewRepository.findApprovedByProduct(productId, limit).catch(() => []);
|
|
5
6
|
});
|
|
6
7
|
export const getReviewsForStore = cache(async (storeId, page = 1, pageSize = 20) => {
|
|
7
8
|
return reviewRepository.listForStore(storeId, { page, pageSize }).catch(() => ({
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* created as fully public/active documents so testers exercise the real browse/search/
|
|
4
4
|
* checkout code paths, but must stay invisible to non-testers. Filtering happens at the
|
|
5
5
|
* application layer (post-fetch, in-memory) rather than via a Firestore `where` clause,
|
|
6
|
-
* because `isTestData` is a new optional field — a Firestore inequality filter
|
|
7
|
-
* (
|
|
8
|
-
*
|
|
6
|
+
* because `isTestData` is a new optional field — a Firestore inequality filter on it
|
|
7
|
+
* (a `!=` clause) would silently exclude every pre-existing document that doesn't have
|
|
8
|
+
* the field set at all, which is worse than the problem it solves.
|
|
9
9
|
*/
|
|
10
10
|
import { isAdminUser } from "../../../../features/auth/role-predicates";
|
|
11
11
|
/** True when the viewer is allowed to see isTestData:true documents. */
|