@doswiftly/storefront-operations 7.0.0 → 7.1.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doswiftly/storefront-operations",
3
- "version": "7.0.0",
3
+ "version": "7.1.0",
4
4
  "description": "GraphQL operations for DoSwiftly Storefront - SSOT from backend",
5
5
  "homepage": "https://doswiftly.pl",
6
6
  "publishConfig": {
@@ -14,6 +14,9 @@
14
14
  "./fragments.graphql": "./fragments.graphql",
15
15
  "./*.graphql": "./*.graphql"
16
16
  },
17
+ "devDependencies": {
18
+ "graphql": "^16.13.2"
19
+ },
17
20
  "keywords": [
18
21
  "graphql",
19
22
  "operations",
@@ -28,11 +31,15 @@
28
31
  "queries.graphql",
29
32
  "mutations.graphql",
30
33
  "fragments.graphql",
34
+ "operations.json",
31
35
  "README.md",
36
+ "AGENTS.md",
37
+ "llms-full.txt",
32
38
  "CHANGELOG.md"
33
39
  ],
34
40
  "scripts": {
35
41
  "sync": "node scripts/sync-operations.js",
36
- "build": "npm run sync"
42
+ "build": "npm run sync",
43
+ "test": "node --test scripts/__tests__/*.test.js"
37
44
  }
38
45
  }
package/queries.graphql CHANGED
@@ -7,6 +7,7 @@
7
7
  # Shop
8
8
  # ============================================
9
9
 
10
+ # Returns shop configuration: name, base + supported currencies, supported locales, branding (logo, colors, fonts, social links), contact info, active payment methods, brand metadata, money format template, and the list of countries the shop ships to. Public; no auth required. Call once per session and cache — almost everything else is contextualized by the shop returned here.
10
11
  query Shop {
11
12
  shop {
12
13
  ...Shop
@@ -17,16 +18,14 @@ query Shop {
17
18
  # Products
18
19
  # ============================================
19
20
 
21
+ # Fetches a single product by `id` or `handle` (URL slug). Pass either — whichever is provided wins; if both are missing, returns null. Returns null if the product is not storefront-accessible (must be `ACTIVE` status with `PUBLIC` or `BUNDLE_ONLY` visibility).
20
22
  query Product($id: ID, $handle: String) {
21
23
  product(id: $id, handle: $handle) {
22
24
  ...ProductFull
23
25
  }
24
26
  }
25
27
 
26
- # Single-query configurator fetch (Faza 1 + Faza 1.5 Decision D-A v3).
27
- # Returns product details + union of set/scoped AttributeDefinitions filtered by CUSTOMER fillingMode.
28
- # Showcase: hurtowniakopiarek.pl Konica Bizhub C258 — 4 customer text fields (from shared set)
29
- # + Finiszer/Podstawa/Podajnik ADF (per-product scoped z linkedVariantId).
28
+ # Fetches a product together with its filtered attribute definitions, optimized for the configurator UI (e.g. customer-facing text fields, finishing options, scoped variants). `fillingMode: "CUSTOMER"` returns only customer-facing attributes; pass `"BOTH"` to also include attributes shared with the merchant admin. Single round-trip — saves a separate `attributes` query.
30
29
  query ProductConfigurator($handle: String!, $fillingMode: String = "CUSTOMER") {
31
30
  product(handle: $handle) {
32
31
  ...ProductFull
@@ -36,6 +35,7 @@ query ProductConfigurator($handle: String!, $fillingMode: String = "CUSTOMER") {
36
35
  }
37
36
  }
38
37
 
38
+ # Paginated product list (Relay Connection, default page size 20, max 100). The `query` argument supports a structured search syntax — `tag:summer`, `vendor:foo`, `product_type:shirts`, `variants.price:>10`, plus `AND`/`OR`/`NOT` — falling back to free-text title/content search. The `filters[]` array uses multi-filter logic: same field name appears multiple times → OR; different fields → AND. Sort: `RELEVANCE`, `TITLE`, `PRICE`, `NEWEST`, `OLDEST`, `BEST_SELLING`. The response includes a `filters` block for faceted navigation (counts per filterable attribute value).
39
39
  query Products(
40
40
  $first: Int = 20
41
41
  $after: String
@@ -61,6 +61,7 @@ query Products(
61
61
  }
62
62
  }
63
63
 
64
+ # Full-text product search — `$query` is required. Functionally equivalent to `Products` with `$query` set, minus the `sortKey` argument (search defaults to relevance ranking). Use for the search results page; combine with `filters[]` for guided refinement.
64
65
  query ProductSearch($query: String!, $first: Int = 20, $after: String, $filters: [ProductFilter!]) {
65
66
  products(query: $query, first: $first, after: $after, filters: $filters) {
66
67
  edges {
@@ -79,6 +80,7 @@ query ProductSearch($query: String!, $first: Int = 20, $after: String, $filters:
79
80
  }
80
81
  }
81
82
 
83
+ # Type-ahead suggestions for the storefront search input. Returns up to `$limit` matching products (hard cap 50) plus up to 5 styled query suggestions with `<mark>` tags around matched spans. Polish-language aware (handles morphology in suggestions). Run on each keystroke (debounce 200-300ms). The `$query` is capped at 100 characters server-side.
82
84
  query PredictiveSearch($query: String!, $limit: Int = 10) {
83
85
  predictiveSearch(query: $query, limit: $limit) {
84
86
  products {
@@ -95,6 +97,7 @@ query PredictiveSearch($query: String!, $limit: Int = 10) {
95
97
  # Collections
96
98
  # ============================================
97
99
 
100
+ # Fetches a single collection by `id` or `handle`, with paginated products. Collections come in two types: **MANUAL** (curated — products explicitly added by the merchant) and **AUTO** (rule-based — products matched dynamically). Both surfaces use the same field selection.
98
101
  query Collection($id: ID, $handle: String, $productsFirst: Int = 20, $productsAfter: String, $productsFilters: [ProductFilter!]) {
99
102
  collection(id: $id, handle: $handle) {
100
103
  ...Collection
@@ -116,6 +119,7 @@ query Collection($id: ID, $handle: String, $productsFirst: Int = 20, $productsAf
116
119
  }
117
120
  }
118
121
 
122
+ # Paginated list of all active collections (default 20, max 100). Sort by `TITLE` or `UPDATED_AT` via `sortKey`. Note: the `query` argument is reserved for future text filtering — it is currently accepted but ignored.
119
123
  query Collections($first: Int = 20, $after: String, $query: String, $sortKey: CollectionSortKeys = TITLE, $reverse: Boolean = false) {
120
124
  collections(first: $first, after: $after, query: $query, sortKey: $sortKey, reverse: $reverse) {
121
125
  edges {
@@ -135,6 +139,7 @@ query Collections($first: Int = 20, $after: String, $query: String, $sortKey: Co
135
139
  # Categories
136
140
  # ============================================
137
141
 
142
+ # Fetches a single category by `id` or `slug` with its parent and immediate children. Use for breadcrumbs and sub-navigation. Nested queries on `parent` / `children` are batched server-side — safe to use in lists without N+1 concerns.
138
143
  query Category($id: ID, $slug: String) {
139
144
  category(id: $id, slug: $slug) {
140
145
  ...Category
@@ -147,6 +152,7 @@ query Category($id: ID, $slug: String) {
147
152
  }
148
153
  }
149
154
 
155
+ # Returns active categories for the shop. Each category exposes its `parent` and `children` — build the tree client-side by walking those fields (server batches the lookups, no N+1). The hierarchy is not depth-capped server-side. Use for nav mega-menus and category pages.
150
156
  query Categories {
151
157
  categories {
152
158
  roots {
@@ -166,6 +172,7 @@ query Categories {
166
172
  # Cart
167
173
  # ============================================
168
174
 
175
+ # Fetches a cart by `id` (the value persisted by the SDK in the `cart-id` cookie). The cart query is public — no auth needed to read it — but once a customer logs in and gets associated with the cart, mutations enforce ownership. Returns line items (paginated up to 100), totals, applied discount codes, gift cards, buyer identity, note, attributes, and warnings. Refetch after every cart mutation.
169
176
  query Cart($id: ID!) {
170
177
  cart(id: $id) {
171
178
  ...Cart
@@ -176,6 +183,7 @@ query Cart($id: ID!) {
176
183
  # Customer (requires auth)
177
184
  # ============================================
178
185
 
186
+ # Full customer profile — basic info plus the first 10 addresses and first 10 orders. Heaviest customer query; for narrow use cases prefer `CustomerProfile` (no orders / addresses) or `CustomerOrder` (single order). Returns null if unauthenticated.
179
187
  query Customer {
180
188
  customer {
181
189
  ...Customer
@@ -209,15 +217,14 @@ query Customer {
209
217
  }
210
218
  }
211
219
 
212
- # Lightweight customer profile query (no orders, no addresses list)
213
- # Use for settings/profile pages that only need basic customer info.
220
+ # Lightweight customer profile (no orders, no addresses list). Use for settings / profile pages that only need basic customer info — much cheaper than `Customer`. Returns null if unauthenticated.
214
221
  query CustomerProfile {
215
222
  customer {
216
223
  ...Customer
217
224
  }
218
225
  }
219
226
 
220
- # Single order query more efficient than fetching all customer data
227
+ # Single order by `orderId`. Returns only orders that belong to the authenticated customer (cross-customer access returns null, not an error). Much cheaper than fetching the full `Customer` payload to access one order. Use on the order detail page.
221
228
  query CustomerOrder($orderId: ID!) {
222
229
  customerOrder(orderId: $orderId) {
223
230
  ...Order
@@ -228,6 +235,7 @@ query CustomerOrder($orderId: ID!) {
228
235
  # Checkout
229
236
  # ============================================
230
237
 
238
+ # Fetches a checkout session by `id`. **Important**: `checkoutId` and `cartId` are 1:1 — there is no separate "checkout" record, the response is built dynamically from the cart. Returns line items, addresses, selected shipping rate, available shipping rates + payment methods, applied discount/gift cards (gift card codes are masked for security), and totals (`cost`, `tax`, `paymentDue`). Public read; ownership enforced on mutations. Refetch after every checkout mutation.
231
239
  query Checkout($id: ID!) {
232
240
  checkout(id: $id) {
233
241
  ...Checkout
@@ -235,9 +243,10 @@ query Checkout($id: ID!) {
235
243
  }
236
244
 
237
245
  # ============================================
238
- # Payment Methods (R23 - Payment Methods)
246
+ # Payment Methods
239
247
  # ============================================
240
248
 
249
+ # Returns the active payment methods for the shop, sorted by the merchant-configured display position. Shop-level — does NOT vary by cart amount or currency. Each method exposes `type` (`CARD`, `BANK_TRANSFER`, `BLIK`, `PAYPAL`, `APPLE_PAY`, `GOOGLE_PAY`, `CASH_ON_DELIVERY`, `OTHER`), provider, icon, description, and supported currencies. Use to render the payment step of checkout.
241
250
  query AvailablePaymentMethods {
242
251
  availablePaymentMethods {
243
252
  ...AvailablePaymentMethods
@@ -245,9 +254,10 @@ query AvailablePaymentMethods {
245
254
  }
246
255
 
247
256
  # ============================================
248
- # Shipments (R29 - Shipment Tracking)
257
+ # Shipments / Tracking
249
258
  # ============================================
250
259
 
260
+ # Fetches a shipment by `id` with status, tracking events, recipient address, and shipped/delivered timestamps. **Auth required** — customer access token plus ownership of the parent order. Wrapped response: `{ shipment, userErrors[] }`. Error codes: `INVALID_TOKEN`, `NOT_FOUND` (also returned on ownership mismatch to prevent enumeration), `FETCH_FAILED`.
251
261
  query Shipment($id: ID!) {
252
262
  shipment(id: $id) {
253
263
  shipment {
@@ -259,6 +269,7 @@ query Shipment($id: ID!) {
259
269
  }
260
270
  }
261
271
 
272
+ # **Public** shipment lookup by carrier tracking number — no auth required. Designed for "Track my order" landing pages reachable without login. Returns the basic shipment fragment including recipient address. Wrapped response: `{ shipment, userErrors[] }`. Error codes: `INVALID_INPUT`, `NOT_FOUND`, `FETCH_FAILED`.
262
273
  query ShipmentByTrackingNumber($trackingNumber: String!) {
263
274
  shipmentByTrackingNumber(trackingNumber: $trackingNumber) {
264
275
  shipment {
@@ -271,9 +282,10 @@ query ShipmentByTrackingNumber($trackingNumber: String!) {
271
282
  }
272
283
 
273
284
  # ============================================
274
- # Returns (R30 - Returns/RMA)
285
+ # Returns / RMA
275
286
  # ============================================
276
287
 
288
+ # Fetches a single return (RMA) by `id` with line items, refund/compensation info, and history. **Auth required** — customer access token plus ownership of the return. Wrapped response: `{ return, userErrors[] }`. Error codes: `INVALID_TOKEN`, `NOT_FOUND` (also returned on ownership mismatch), `FETCH_FAILED`.
277
289
  query Return($id: ID!) {
278
290
  return(id: $id) {
279
291
  return {
@@ -285,6 +297,7 @@ query Return($id: ID!) {
285
297
  }
286
298
  }
287
299
 
300
+ # Lists returns for a given order (paginated, default page size 20, cursor-based). **Auth required** — customer access token plus ownership of the order; the connection is empty (no explicit error) on auth failure. Use on the order detail page to show return history.
288
301
  query ReturnsByOrder($orderId: ID!) {
289
302
  returnsByOrder(orderId: $orderId) {
290
303
  edges {
@@ -300,6 +313,7 @@ query ReturnsByOrder($orderId: ID!) {
300
313
  }
301
314
  }
302
315
 
316
+ # Returns the standard list of return reasons used by the RMA flow: `DEFECTIVE`, `NOT_AS_DESCRIBED`, `WRONG_ITEM`, `CHANGED_MIND`, `BETTER_PRICE`, `DAMAGED_SHIPPING`, `OTHER`. The list is fixed across all shops — not per-shop configurable. Public; no auth required.
303
317
  query ReturnReasons {
304
318
  returnReasons {
305
319
  ...ReturnReasonOption
@@ -307,15 +321,17 @@ query ReturnReasons {
307
321
  }
308
322
 
309
323
  # ============================================
310
- # Gift Cards (R32 - Gift Cards)
324
+ # Gift Cards
311
325
  # ============================================
312
326
 
327
+ # Public gift-card lookup by `code`. Returns balance, currency, expiry, and `maskedCode` (first 4 + last 4 chars only — the full code never leaks back). Returns null if the code is unknown (rather than an explicit error, to limit enumeration). **Rate-limited**: 10 requests per 60 seconds per IP.
313
328
  query GiftCard($code: String!) {
314
329
  giftCard(code: $code) {
315
330
  ...GiftCard
316
331
  }
317
332
  }
318
333
 
334
+ # Validates whether a gift card is usable (and optionally for a given `$amount`). Checks status (`DISABLED`, `USED`, `EXPIRED`), expiry date, and — when `$amount` is provided — sufficient balance. Returns `{ validation: { isValid, availableBalance, error: { code, message } }, userErrors[] }`. Validation error codes: `NOT_FOUND`, `DISABLED`, `ALREADY_USED`, `EXPIRED`, `INSUFFICIENT_BALANCE`. **Rate-limited**: 10 / 60s.
319
335
  query GiftCardValidate($code: String!, $amount: Float) {
320
336
  giftCardValidate(code: $code, amount: $amount) {
321
337
  validation {
@@ -328,9 +344,10 @@ query GiftCardValidate($code: String!, $amount: Float) {
328
344
  }
329
345
 
330
346
  # ============================================
331
- # Shipping Methods (R33 - Shipping Methods)
347
+ # Shipping Methods
332
348
  # ============================================
333
349
 
350
+ # Returns shipping methods for a given destination address and cart shape (subtotal, total weight, currency). The query computes everything from the inputs alone — no existing cart is required, so it can be used for "shipping cost preview" UIs before the customer adds anything to a cart. Each method includes price, free-shipping progress (`{ qualifies, currentAmount, threshold, remaining, progressPercent }`), estimated delivery, and carrier metadata. Sorted by the merchant's `sortOrder`, then by price.
334
351
  query AvailableShippingMethods($address: ShippingAddressInput!, $cart: CartShippingInput) {
335
352
  availableShippingMethods(address: $address, cart: $cart) {
336
353
  methods {
@@ -346,9 +363,10 @@ query AvailableShippingMethods($address: ShippingAddressInput!, $cart: CartShipp
346
363
  }
347
364
 
348
365
  # ============================================
349
- # Attribute Filters (R35 - Dynamic Attributes)
366
+ # Attribute Filters
350
367
  # ============================================
351
368
 
369
+ # Returns the dynamic facet filters available for a listing context — pass `collectionId`, `categoryId`, or `searchQuery`. For each visible & filterable attribute, returns either discrete value counts (for `SELECT` / `CHECKBOX` types) or numeric range bounds (for `SLIDER` types). Plus `priceRange`, per-category counts, `activeCount` (length of `currentFilters` input), and `matchCount` (total products in the context). Use to render filter sidebars on listing/search pages.
352
370
  query AvailableFilters($input: AvailableFiltersInput) {
353
371
  availableFilters(input: $input) {
354
372
  ...AvailableFilters
@@ -356,27 +374,31 @@ query AvailableFilters($input: AvailableFiltersInput) {
356
374
  }
357
375
 
358
376
  # ============================================
359
- # Loyalty Program (R7, R8, R10.4)
377
+ # Loyalty Program
360
378
  # ============================================
361
379
 
380
+ # Returns the logged-in customer's loyalty membership: points (current, pending, redeemed, expired, expiring), current tier, tier progress, annual spend, last activity. Returns null if the customer is not enrolled — there is **no auto-enrollment** here (enrollment happens via signup or a first qualifying order). Auth required.
362
381
  query LoyaltyMember {
363
382
  loyaltyMember {
364
383
  ...LoyaltyMember
365
384
  }
366
385
  }
367
386
 
387
+ # Lists the loyalty tiers configured for the shop (`BRONZE`, `SILVER`, `GOLD`, `PLATINUM`, `DIAMOND` etc.) with their `minPoints`, `minAnnualSpend`, `pointsMultiplier`, and custom benefits. Sorted by `minPoints` ASC. Public; no auth required.
368
388
  query LoyaltyTiers {
369
389
  loyaltyTiers {
370
390
  ...LoyaltyTier
371
391
  }
372
392
  }
373
393
 
394
+ # Lists rewards customers can redeem (free shipping, percent off, free product, gift card). Filtered to **active** rewards only (`is_active = true` AND inside their `starts_at`/`ends_at` window). Public; no auth required.
374
395
  query LoyaltyRewards {
375
396
  loyaltyRewards {
376
397
  ...LoyaltyReward
377
398
  }
378
399
  }
379
400
 
401
+ # Paginated history of loyalty point transactions for the logged-in customer (default 20). Transaction `type` enum: `EARN_PURCHASE`, `EARN_SIGNUP`, `EARN_REFERRAL`, `EARN_REVIEW`, `EARN_BIRTHDAY`, `EARN_BONUS`, `REDEEM`, `EXPIRE`, `ADJUST`, `REFUND_REVERSAL`. Auth required — empty connection if unauthenticated.
380
402
  query LoyaltyTransactions($first: Int = 20, $after: String) {
381
403
  loyaltyTransactions(first: $first, after: $after) {
382
404
  edges {
@@ -392,18 +414,21 @@ query LoyaltyTransactions($first: Int = 20, $after: String) {
392
414
  }
393
415
  }
394
416
 
417
+ # Returns the loyalty program configuration: `isEnabled`, `pointsName` (e.g. "stars"), `pointsPerCurrency`, `pointsExpiryMonths`, available earn actions, referral settings. Use this at app boot to decide whether to render any loyalty UI at all. Public; no auth required.
395
418
  query LoyaltySettings {
396
419
  loyaltySettings {
397
420
  ...LoyaltySettings
398
421
  }
399
422
  }
400
423
 
424
+ # Estimates how many loyalty points the customer would earn for an order of `$orderTotal` (in major currency units). When the customer is authenticated, the result accounts for their current tier's points multiplier. Use on cart/checkout to show "Earn X points with this order".
401
425
  query EstimatePoints($orderTotal: Float!) {
402
426
  estimatePoints(orderTotal: $orderTotal) {
403
427
  ...PointsEstimate
404
428
  }
405
429
  }
406
430
 
431
+ # Returns the customer's referral statistics: `referralCode`, `shareUrl`, `totalReferred`, `completedReferrals`, `pendingReferrals`, `totalPointsEarned`. Auth required. Returns null if unauthenticated or if the referral program is disabled for the shop.
407
432
  query ReferralStats {
408
433
  referralStats {
409
434
  ...ReferralStats
@@ -414,6 +439,7 @@ query ReferralStats {
414
439
  # Reviews
415
440
  # ============================================
416
441
 
442
+ # Paginated list of customer reviews for a product, **filtered to APPROVED reviews only** (PENDING / REJECTED reviews are not exposed to the storefront). Sort by `CREATED_AT` (default), helpfulness, or rating. Public; no auth required.
417
443
  query ProductReviews($productId: ID!, $first: Int = 10, $after: String, $sortKey: ReviewSortKey = CREATED_AT, $reverse: Boolean = true) {
418
444
  productReviews(productId: $productId, first: $first, after: $after, sortKey: $sortKey, reverse: $reverse) {
419
445
  edges {
@@ -429,6 +455,7 @@ query ProductReviews($productId: ID!, $first: Int = 10, $after: String, $sortKey
429
455
  }
430
456
  }
431
457
 
458
+ # Aggregate review statistics for a product: average rating, total count, distribution per star (1-5). Computed from APPROVED reviews only. Use for product card review summaries. Public; no auth required.
432
459
  query ReviewStats($productId: ID!) {
433
460
  reviewStats(productId: $productId) {
434
461
  ...ReviewStats
@@ -439,6 +466,7 @@ query ReviewStats($productId: ID!) {
439
466
  # Wishlists
440
467
  # ============================================
441
468
 
469
+ # Paginated list of the logged-in customer's wishlists (default 20). Auth required — empty connection if unauthenticated. Customers typically have a small set (<10).
442
470
  query Wishlists($first: Int = 20, $after: String) {
443
471
  wishlists(first: $first, after: $after) {
444
472
  edges {
@@ -460,6 +488,7 @@ query Wishlists($first: Int = 20, $after: String) {
460
488
  }
461
489
  }
462
490
 
491
+ # Fetches a single wishlist by `id`. Private wishlists are visible only to the owner; public wishlists are visible to anyone. Note: this query supports lookup by `id` only — there is currently no way to fetch a wishlist by its share token.
463
492
  query WishlistById($id: ID!) {
464
493
  wishlist(id: $id) {
465
494
  ...Wishlist
@@ -470,6 +499,7 @@ query WishlistById($id: ID!) {
470
499
  # Blog
471
500
  # ============================================
472
501
 
502
+ # Paginated list of published blog posts. Filter by `categorySlug`, `tagSlug`, or `featured` (boolean flag, not enum). Sort: `PUBLISHED_AT` (default), `TITLE`, `VIEW_COUNT`, or `CREATED_AT`. Public; no auth required.
473
503
  query BlogPosts($first: Int = 20, $after: String, $categorySlug: String, $tagSlug: String, $featured: Boolean, $sortKey: BlogPostSortKey = PUBLISHED_AT, $reverse: Boolean = false) {
474
504
  blogPosts(first: $first, after: $after, categorySlug: $categorySlug, tagSlug: $tagSlug, featured: $featured, sortKey: $sortKey, reverse: $reverse) {
475
505
  edges {
@@ -485,18 +515,21 @@ query BlogPosts($first: Int = 20, $after: String, $categorySlug: String, $tagSlu
485
515
  }
486
516
  }
487
517
 
518
+ # Fetches a single blog post by `id` or `slug`. Visibility-gated: returns null if the post is not yet `PUBLISHED` or if its publish date is in the future (scheduled posts stay hidden until their publish time). Side effect: fetching a post increments its `view_count` asynchronously (does not block the response).
488
519
  query BlogPost($id: ID, $slug: String) {
489
520
  blogPost(id: $id, slug: $slug) {
490
521
  ...BlogPost
491
522
  }
492
523
  }
493
524
 
525
+ # Lists all blog categories with per-category `postCount` and SEO metadata. Use to render category navigation on blog pages. Public; no auth required.
494
526
  query BlogCategories {
495
527
  blogCategories {
496
528
  ...BlogCategory
497
529
  }
498
530
  }
499
531
 
532
+ # Lists blog tags with usage counts (`postCount` per tag). Use to render a tag cloud. Public; no auth required.
500
533
  query BlogTags {
501
534
  blogTags {
502
535
  ...BlogTag
@@ -507,6 +540,7 @@ query BlogTags {
507
540
  # Recommendations
508
541
  # ============================================
509
542
 
543
+ # Returns up to `$limit` recommended products related to `$productId`. Default `$intent: SIMILAR` — products sharing categories or tags. Use on PDP "You may also like" sections. Public; no auth required.
510
544
  query ProductRecommendations($productId: ID!, $limit: Int = 8, $intent: RecommendationIntent = SIMILAR) {
511
545
  productRecommendations(productId: $productId, limit: $limit, intent: $intent) {
512
546
  ...ProductCard
@@ -517,12 +551,14 @@ query ProductRecommendations($productId: ID!, $limit: Int = 8, $intent: Recommen
517
551
  # Content: Pages
518
552
  # ============================================
519
553
 
554
+ # Fetches a single CMS page (About, Privacy, Returns Policy, Terms, etc.) by `handle` or `id`. Visibility-gated: returns null if the page is hidden or if its publish date is in the future. Public; no auth required.
520
555
  query Page($handle: String, $id: ID) {
521
556
  page(handle: $handle, id: $id) {
522
557
  ...ShopPage
523
558
  }
524
559
  }
525
560
 
561
+ # Paginated list of visible, already-published CMS pages. Use for sitemap, footer link list, or page directory. The `query` argument supports text search over the page title/handle. Public; no auth required.
526
562
  query Pages($first: Int = 20, $after: String, $sortKey: PageSortKeys = TITLE, $reverse: Boolean = false, $query: String) {
527
563
  pages(first: $first, after: $after, sortKey: $sortKey, reverse: $reverse, query: $query) {
528
564
  edges {
@@ -542,6 +578,7 @@ query Pages($first: Int = 20, $after: String, $sortKey: PageSortKeys = TITLE, $r
542
578
  # Content: Navigation Menus
543
579
  # ============================================
544
580
 
581
+ # Fetches a navigation menu by `handle` (e.g. `"main-menu"`, `"footer"`, `"mobile"`). Returns the nested item tree. Each item is typed as one of: `HTTP`, `FRONTPAGE`, `SEARCH`, `CATALOG`, `BLOG`, `PRODUCT`, `COLLECTION`, `CATEGORY`, or `PAGE` — switch on the type to render the right link target. Linked resources and URLs are resolved on demand by the field selections in the `Menu` fragment.
545
582
  query Menu($handle: String!) {
546
583
  menu(handle: $handle) {
547
584
  ...Menu
@@ -552,6 +589,7 @@ query Menu($handle: String!) {
552
589
  # Content: URL Redirects
553
590
  # ============================================
554
591
 
592
+ # Returns the shop's URL redirects (legacy `path` → new `target` mappings). Use server-side at the edge or in SSR to issue 301 redirects for migrated routes (preserves SEO equity). Default page size 250 — most shops fit in a single page.
555
593
  query UrlRedirects($first: Int = 250, $after: String) {
556
594
  urlRedirects(first: $first, after: $after) {
557
595
  nodes {
@@ -566,9 +604,8 @@ query UrlRedirects($first: Int = 250, $after: String) {
566
604
  # ============================================
567
605
  # Store Availability: per-location stock (BOPIS / multi-location)
568
606
  # ============================================
569
- # Field returns null for single-location shops (storefront can skip the store picker).
570
- # `near`, `locationType`, and `@inContext(preferredLocationId)` shape the ordering.
571
607
 
608
+ # Fetches a product (by `handle` or `id`) along with per-variant availability across the merchant's physical locations — for the BOPIS / multi-location flow. The `storeAvailability` connection lives on each `ProductVariant`; its arguments (`first`, `after`, `near`, `locationType`) are set inside the `VariantStoreAvailability` fragment. The connection returns null for single-location shops (in which case the storefront can skip the store picker entirely). `availableStock` is null for anonymous users and an integer for authenticated customers. Apply `@inContext(preferredLocationId: ...)` on the operation to pin the customer's preferred location to the top of the result.
572
609
  query ProductStoreAvailability($handle: String, $id: ID) {
573
610
  product(handle: $handle, id: $id) {
574
611
  id
@@ -584,6 +621,7 @@ query ProductStoreAvailability($handle: String, $id: ID) {
584
621
  # Locations (store picker UI)
585
622
  # ============================================
586
623
 
624
+ # Paginated list of active store locations (default 20, max 100). Filters: `near` (`{ latitude, longitude }`) for proximity search — sorts ascending by distance; `hasPickupEnabled` for pickup-only filtering; `locationType` (`RETAIL`, `WAREHOUSE`, `PICKUP_POINT`). When `near` is omitted, results are sorted by the merchant's `priority`, then name. Use for the BOPIS store picker UI. Public; no auth required.
587
625
  query Locations(
588
626
  $first: Int = 20
589
627
  $after: String
@@ -611,6 +649,7 @@ query Locations(
611
649
  }
612
650
  }
613
651
 
652
+ # Fetches a single store location by `id` — full address, coordinates, business hours, pickup config (lead time, hours, timezone), and services. Returns null if the location is not found, not active, or owned by another shop. Use on the location detail page. Public; no auth required.
614
653
  query Location($id: ID!) {
615
654
  location(id: $id) {
616
655
  ...Location