← Files WixARCHIVED FILE
skills/wix-app/references/stores/QUERY.md
4.42 KB · Oct 8, 2026 · 12:02 UTC
# Stores — Query / List Products
## List products with pagination
Both versions expose a fluent query builder, but the paging method differs:
| Aspect | V1 (`products.queryProducts()`) | V3 (`productsV3.queryProducts()`) |
|--------|--------------------------------|----------------------------------|
| API style | Fluent builder: `.skip().limit().find()` | Fluent builder: `.skipTo(cursor).limit().find()` |
| Pagination | Offset (`.skip(n)`) | Cursor (`.skipTo(cursor)`) |
| Result `items` | `res.items` (V1 `Product[]`) | `res.items` (V3 `productsV3.V3Product[]`) |
| Total count | `res.totalCount` | **None** — V3 only has `cursors.next` + `hasNext()` |
| `hasNext` | `res.hasNext()` (method) | `res.hasNext()` (method) |
| Next cursor | n/a | `res.cursors.next` (string) |
```typescript
import { catalogVersioning, products, productsV3 } from '@wix/stores';
export interface ProductsPage {
products: unknown[]; // narrow at call site
nextCursor: string | null; // V3 only
hasNext: boolean;
totalCount: number | null; // V1 only
}
export async function listProductsPage(
limit: number,
cursorOrSkip: string | number | undefined,
): Promise<ProductsPage> {
const v = await getVersion();
if (v === 'STORES_NOT_INSTALLED') {
return { products: [], nextCursor: null, hasNext: false, totalCount: 0 };
}
if (v === 'V3_CATALOG') {
let builder = productsV3.queryProducts().limit(limit);
if (typeof cursorOrSkip === 'string') builder = builder.skipTo(cursorOrSkip);
// Do NOT chain a sort — see [STORES_VERSIONING.md](../STORES_VERSIONING.md) gotcha #10.
const res = await builder.find();
return {
products: res.items,
nextCursor: res.cursors.next ?? null,
hasNext: res.hasNext(),
totalCount: null,
};
}
const skip = typeof cursorOrSkip === 'number' ? cursorOrSkip : 0;
const res = await products.queryProducts().skip(skip).limit(limit).find();
return {
products: res.items,
nextCursor: null,
hasNext: res.hasNext(),
totalCount: res.totalCount ?? null,
};
}
```
> **Two ways to call V3 `queryProducts`:** the canonical builder shown above, and a direct-call form `productsV3.queryProducts({ cursorPaging: { limit, cursor } })` returning a `Promise<{ products, pagingMetadata }>`. Both compile and run, but the builder is more idiomatic and matches V1's shape.
---
## Search products by name
V3 `queryProducts` filters only on `_id`, `slug`, `options.id` and `handle` — `name` is a
compile error. Use `productsV3.searchProducts()`, a direct call rather than a builder. V1 has
no search method; its builder accepts `name` via `startsWith` (prefix match only, no `fuzzy`).
Imports come from the first example in this file.
```typescript
// `v` comes from getVersion() — see STORES_VERSIONING.md.
export async function searchByName(
term: string,
limit: number,
v: 'V1_CATALOG' | 'V3_CATALOG' | 'STORES_NOT_INSTALLED',
) {
if (v === 'V3_CATALOG') {
const res = await productsV3.searchProducts({
cursorPaging: { limit },
// `fields` is required for free-text search. Searchable: 'name', 'description',
// 'variantsInfo.variants.sku', 'minVariantPriceInfo.sku'.
search: { expression: term, fields: ['name'], fuzzy: true },
});
// Note `products` (not `items`) and `pagingMetadata.hasNext` (a property, not a method).
return res.products ?? [];
}
const res = await products.queryProducts().startsWith('name', term).limit(limit).find();
return res.items;
}
```
---
## Display price/stock without fetching variants
V3 `queryProducts` does not return variants. Read product-level rollup fields instead:
```typescript
function displayPrice(p: { actualPriceRange?: { minValue?: { amount?: string }; maxValue?: { amount?: string } } }): string {
const min = p.actualPriceRange?.minValue?.amount;
const max = p.actualPriceRange?.maxValue?.amount;
if (!min) return '—';
return max && max !== min ? `${min} – ${max}` : min;
}
function stockLabel(status: string | undefined): string {
switch (status) {
case 'IN_STOCK': return 'In Stock';
case 'OUT_OF_STOCK': return 'Out of Stock';
case 'PARTIALLY_OUT_OF_STOCK': return 'Limited';
case 'PREORDER': return 'Pre-order';
default: return '—';
}
}
// V1 path: product.stock.inventoryStatus (same UPPER_SNAKE_CASE values)
// V3 path: product.inventory.availabilityStatus (adds PREORDER)
// SKU lives on the variant — show "—" in lists, or use Read-Only Variants API.
```
SHA-256: 7b19e2d2bedb40ac3e5a1c8afb9f573c188fa79d83fdd855832dc437fd905c91