Przejdź do głównej zawartości

Migracja do Storefront API 5.0

Przewodnik migracji ze Storefront API 4.x do 5.0. Wszystkie zmiany są breaking — refaktoring eliminuje legacy patterns i wprowadza spójne nazewnictwo end-to-end.

Quick checklist
  1. Zaktualizuj @doswiftly/cli, @doswiftly/storefront-sdk, @doswiftly/storefront-operations do ^5.0.0
  2. Uruchom codegen lokalnie — TypeScript wymusi większość renames przez compile errors
  3. Zaktualizuj queries/mutations według tabel poniżej
  4. Zaktualizuj komponenty czytające addresses (US/CA → international naming)
  5. Zaktualizuj UI używające cart.subtotalPrice / order.totalPrice na nested cost.subtotal / totals.total
  6. Sprawdź auth flow — token przechowywany teraz w httpOnly cookie, nie localStorage

Spis treści

Storefront API nie akceptuje już customerAccessToken jako argumentu mutation/query. Token żyje w httpOnly cookie (browser auto-send) lub Authorization: Bearer header (mobile/server-to-server).

# Before (4.x)
query Customer($customerAccessToken: String!) {
customer(customerAccessToken: $customerAccessToken) {
id
email
}
}

# After (5.x)
query Customer {
customer {
id
email
}
}

Po zalogowaniu (customerLogin), API zwraca Set-Cookie: customerAccessToken=<jwt>; HttpOnly; SameSite=Lax; Secure. Browser automatycznie dołącza cookie do każdego subsequent requestu (wymaga credentials: 'include' w fetch — SDK robi to automatycznie). Mobile/server-to-server może dalej czytać accessToken z body response.

Hydration po page refresh: useAuthStore nie persistuje już accessToken w localStorage (XSS hardening). Template ma nową BFF route /api/auth/whoami która odczytuje cookie i zwraca customer info — useAuthSync hook ją wywołuje na mount.

Zmiana w SDK: AuthClient.logout(), renewToken(), getCustomer() nie przyjmują już token argumentu (auth context resolved per-request).

Pełna dokumentacja: SDK customer-auth.

Adresy: international naming

Pola adresowe używają teraz international neutralnej terminologii zamiast US/Canadian-centric nazewnictwa.

Przed (4.x)Po (5.x)
address1streetLine1
address2streetLine2
provincestate
provinceCodestateCode
zippostalCode
# Before
fragment MailingAddress on MailingAddress {
address1
address2
province
zip
}

# After
fragment MailingAddress on MailingAddress {
streetLine1
streetLine2
state
postalCode
}

Backward-compat dla istniejących orders: Order.shippingAddress / billingAddress są persistowane jako snapshots przy złożeniu zamówienia. Snapshots z poprzednich wersji API zawierają address1/province/zip; nowe używają streetLine1/state/postalCode. API czyta nowe nazwy najpierw, fallbackuje do legacy — historyczne zamówienia renderują się poprawnie bez migracji danych. Fallback zostanie usunięty gdy historyczne snapshots osiągną end-of-life (~2-3 lata).

Pole MailingAddress.formatted (country-aware ordered lines) działa bez zmian — internal helper zaktualizowany na nowe nazwy.

Wpływa na: MailingAddress, MailingAddressInput, CheckoutAddressInput, ShopAddress, ShippingAddressInput, LocationAddress.

Costs/Totals: nested wrappers

Cart, Checkout i Order mają teraz dedicated wrapper types dla cost breakdown'ów. Symetria między 3 etapami flow'u + dodawanie nowych cost dimensions (duty, fees) jest non-breaking.

Cart

# Before — flat fields with "Amount" suffix
cart {
cost {
subtotalAmount { amount currencyCode }
totalAmount { amount currencyCode }
totalTaxAmount { amount currencyCode }
}
lines(first: 10) {
nodes {
cost {
amountPerQuantity { amount }
subtotalAmount { amount }
totalAmount { amount }
}
}
}
}

# After — drop "Amount" suffix, "amountPerQuantity" → "pricePerUnit"
cart {
cost {
subtotal { amount currencyCode }
total { amount currencyCode }
totalTax { amount currencyCode }
}
lines(first: 10) {
nodes {
cost {
pricePerUnit { amount }
subtotal { amount }
total { amount }
}
}
}
}

Checkout

# Before — flat fields
checkout {
subtotalPrice { amount currencyCode }
totalPrice { amount currencyCode }
totalTax { amount currencyCode }
totalShippingPrice { amount currencyCode }
totalDiscounts { amount currencyCode }
}

# After — nested cost wrapper (CheckoutCost)
checkout {
cost {
subtotal { amount currencyCode }
total { amount currencyCode }
totalTax { amount currencyCode }
totalShipping { amount currencyCode }
totalDiscounts { amount currencyCode }
}
}

Order

# Before — flat fields
order {
subtotalPrice { amount currencyCode }
totalPrice { amount currencyCode }
totalTax { amount currencyCode }
totalShipping { amount currencyCode }
}

# After — nested totals wrapper (OrderTotals)
order {
totals {
subtotal { amount currencyCode }
total { amount currencyCode }
totalTax { amount currencyCode }
totalShipping { amount currencyCode }
}
}

CheckoutLineItem

# Before
checkoutLineItem {
unitPrice { amount }
totalPrice { amount }
}

# After
checkoutLineItem {
pricePerUnit { amount }
total { amount }
}

Conversion-transparency opt-in

Pola *WithConversion (PriceMoney) renamed w lockstep:

  • subtotalAmountWithConversionsubtotalWithConversion
  • totalAmountWithConversiontotalWithConversion
  • amountPerQuantityWithConversionpricePerUnitWithConversion

Cart line: variant zamiast Merchandise union

CartLine.merchandise: Merchandise! (union currently with only ProductVariant member) i BaseCartLine interface zostały usunięte. CartLine jest teraz concrete type z bezpośrednim polem variant: ProductVariant!.

# Before — union + inline fragment
fragment CartLine on CartLine {
id
quantity
merchandise {
... on ProductVariant {
id
title
price { amount currencyCode }
}
}
}

# After — direct field, no union
fragment CartLine on CartLine {
id
quantity
variant {
id
title
price { amount currencyCode }
}
}

Cart connection types renamed: BaseCartLineConnection / BaseCartLineEdgeCartLineConnection / CartLineEdge.

Cart line input renamed merchandiseIdvariantId:

# Before
mutation CartAddLines($id: ID!, $lines: [CartLineInput!]!) {
cartAddLines(id: $id, lines: $lines) { ... }
}
# variables: { lines: [{ merchandiseId: "uuid", quantity: 1 }] }

# After
mutation CartAddLines($id: ID!, $lines: [CartLineInput!]!) {
cartAddLines(id: $id, lines: $lines) { ... }
}
# variables: { lines: [{ variantId: "uuid", quantity: 1 }] }

UserError: jeden generic typ

Per-domain user-error types (CartUserError, CheckoutUserError, CustomerUserError) zastąpione generic UserError używanym wszędzie. Error codes pozostają unique per domain (np. CART_NOT_FOUND vs CUSTOMER_NOT_FOUND vs CHECKOUT_PAYMENT_FAILED) — string comparison na code daje tę samą branching power.

# Before
mutation CartCreate { cartCreate { userErrors { ...CartUserError } } }
mutation CustomerLogin { customerLogin { customerUserErrors { ...CustomerUserError } } }
mutation CheckoutCreate { checkoutCreate { userErrors { ...CheckoutUserError } } }

# After — single fragment everywhere
mutation CartCreate { cartCreate { userErrors { ...UserError } } }
mutation CustomerLogin { customerLogin { userErrors { ...UserError } } }
mutation CheckoutCreate { checkoutCreate { userErrors { ...UserError } } }

Customer mutation payloads renaming pole błędów: customerUserErrorsuserErrors (consistency z innymi payloads).

fragment UserError on UserError {
message
code # namespaced string — np. "CART_NOT_FOUND", "CUSTOMER_TOKEN_INVALID"
field # path do input field które spowodowało błąd
}

Zmiana w TypeScript codegen: userErrors[i].code jest teraz string | null zamiast strongly-typed enum. Switch z code === CartErrorCode.CART_NOT_FOUND na code === 'CART_NOT_FOUND'.

Wishlist + GiftCard: pagination + simplified read

Wishlist Connection

wishlists query zwraca teraz WishlistConnection! (Relay pattern) zamiast [Wishlist!]!.

# Before
query Wishlists {
wishlists { id name itemCount items { id productId } }
}

# After
query Wishlists($first: Int = 20, $after: String) {
wishlists(first: $first, after: $after) {
nodes { id name itemCount items { id productId } }
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
totalCount
}
}

GiftCard read query

giftCard(code) nie wraps wyniku w GiftCardPayload. Zwraca nullable GiftCard bezpośrednio (null = not found). Wrapper pattern zachowany tylko dla mutations.

# Before
query GiftCard($code: String!) {
giftCard(code: $code) {
giftCard { id maskedCode balance { amount currencyCode } }
userErrors { message code }
}
}

# After
query GiftCard($code: String!) {
giftCard(code: $code) { id maskedCode balance { amount currencyCode } }
}

Pole GiftCard.transactions zostało usunięte całkowicie ze storefront API (PII safety — transactions zawierały orderId/customerId, niebezpieczne na anonymous code-lookup endpoint).

Recommendations: field resolvers zamiast top-level queries

Top-level queries similarProducts(productId) i cartRecommendations(cartId) zostały usunięte. Klienci używają field resolvers Product.recommendations i Cart.recommendations. Identyczne payloads, automatic context inheritance, jasna distinction parent-not-found (null) vs empty-recs ([]).

# Before
query SimilarProducts($productId: String!, $first: Int) {
similarProducts(productId: $productId, first: $first) {
items { product { id title } type score }
totalCount
}
}
query CartRecommendations($cartId: String!, $first: Int) {
cartRecommendations(cartId: $cartId, first: $first) {
frequentlyBoughtTogether { product { id } }
youMayAlsoLike { product { id } }
}
}

# After — same payloads, parent-scoped
query Product($id: ID!, $first: Int) {
product(id: $id) {
recommendations(first: $first) {
items { product { id title } type score }
totalCount
}
}
}
query Cart($id: ID!, $first: Int) {
cart(id: $id) {
recommendations(first: $first) {
frequentlyBoughtTogether { product { id } }
youMayAlsoLike { product { id } }
}
}
}

Top-level productRecommendations(productId, intent, limit) (zwracający flat [Product!]! z intent enum SIMILAR/COMPLEMENTARY/UPSELL) i personalizedRecommendations(first) (trending, było recommendations) pozostają.

Filter scalars + Weight type

Filter ID + JSON

Filter.id i FilterValue.id używają teraz ID! zamiast String!. FilterValue.input zmienił typ z String! (JSON-stringified) na JSON! (nowy opaque scalar).

# Before — client-side parsing required
filterValue.input # "{\"variantOption\":{\"name\":\"Color\",\"value\":\"Red\"}}"
JSON.parse(filterValue.input)

# After — directly passable as filters arg element
filterValue.input # { variantOption: { name: "Color", value: "Red" } }

Weight type

ProductVariant.weight zmienił typ z Float (raw grams, implicit) na structured Weight { value: Float!, unit: WeightUnit! }. API zawsze zwraca unit: GRAMS (canonical storage). Storefront może konwertować do preferowanej jednostki bez hardcoding "grams".

# Before
variant { weight } # 250 (Float, "grams" implicit per docs)

# After
variant { weight { value unit } } # { value: 250, unit: GRAMS }

Naming sweep: mutations + fields

Wybrane mutations + field renames (pełna lista w autogenerated CHANGELOG):

Cart mutations

PrzedPo
cartLinesAddcartAddLines
cartLinesUpdatecartUpdateLines
cartLinesRemovecartRemoveLines
cartNoteUpdatecartUpdateNote
cartAttributesUpdatecartUpdateAttributes
cartBuyerIdentityUpdatecartUpdateBuyerIdentity
cartDiscountCodesUpdatecartApplyDiscountCodes

Resource-id arg renamed cartIdid na mutations (foreign-key CheckoutCreateInput.cartId zachowuje nazwę).

Customer mutations

PrzedPo
customerAccessTokenCreatecustomerLogin
customerAccessTokenDeletecustomerLogout
customerAccessTokenRenewcustomerRefreshToken
customerCreatecustomerSignup
customerRecovercustomerRequestPasswordReset
customerResetByUrlcustomerResetPassword
customerActivateByUrlcustomerActivate
customerAddressCreatecustomerAddAddress
customerAddressUpdatecustomerUpdateAddress
customerAddressDeletecustomerRemoveAddress
customerDefaultAddressUpdatecustomerSetDefaultAddress

Checkout mutations

PrzedPo
checkoutShippingAddressUpdatecheckoutUpdateShippingAddress
checkoutBillingAddressUpdatecheckoutUpdateBillingAddress
checkoutEmailUpdatecheckoutUpdateEmail
checkoutShippingLineUpdatecheckoutSelectShippingRate (+ arg shippingRateHandlerateId)
checkoutDiscountCodeApplycheckoutApplyDiscountCode
checkoutDiscountCodeRemovecheckoutRemoveDiscountCode
checkoutGiftCardApplycheckoutApplyGiftCard
checkoutPaymentMethodUpdatecheckoutSelectPaymentMethod

Top-level query renames

PrzedPo
recommendationspersonalizedRecommendations
predictiveSearchsearchSuggestions
productSearchsearchProducts
availableFiltersproductFilters

Field renames na output types

Type.fieldPo
Product.productTypeProduct.category (free-text classification; Product.type: ProductTypeEnum enum field bez zmian)
Product.totalInventoryProduct.stockTotal
Product.collectRecipientInfousunięte (storefront-driven recipient mode — patrz Karty podarunkowe w cart.md)
ProductVariant.quantityAvailableavailableStock
ProductOption.position / ProductOptionValue.positionsortOrder
Customer.numberOfOrdersorderCount
Customer.emailMarketingStateCustomer.emailMarketing (typed enum)
WishlistItem.priceAtAddpriceWhenAdded
Shop.primaryDomain.sslEnabledisSslEnabled (boolean prefix)
Shipment.estimatedDeliveryDateestimatedDeliveryAt
StoreAvailability.pickUpTimepickupTime
Location.pickupEnabledsupportsPickup
ShopPage.bodySummaryexcerpt
BlogPost.contentTypeBlogPost.contentFormat (typed enum HTML/MARKDOWN)
BlogPost.readingTimereadingTimeMinutes
Order.financialStatuspaymentStatus
AvailableFilters.activeFilterCountactiveCount
AvailableFilters.totalProductsmatchCount
Menu.itemsCountusunięte (= items.length)
MenuItem.urltyped URL scalar (było String)
LoyaltyReward.remainingRedemptionsredemptionsRemaining
LoyaltyPageInfousunięte (użyj PageInfo)
Shop.primaryDomain (string) + primaryDomainObject (Domain)primaryDomain: Domain! (jeden field)
GiftCard.lastCharactersusunięte (compute z maskedCode.slice(-4))

Boolean prefix is/has/can/supports

Wszędzie boolean fields → is* / has* / supports*:

  • Product.availableForSaleProduct.isAvailable
  • ProductVariant.availableForSaleProductVariant.isAvailable
  • Product.purchasableisPurchasable
  • Customer.emailVerifiedisEmailVerified
  • Checkout.{ready,completed}{isReady,isCompleted}
  • Discount*.applicableisApplicable
  • *Validation.validisValid
  • Location.pickupEnabledsupportsPickup
  • StoreAvailability.availableisAvailable
  • LoyaltyReward.availableisAvailable

Review vote split

# Before — boolean argument anti-pattern
mutation ReviewVote($reviewId: ID!, $isHelpful: Boolean!) {
reviewVote(reviewId: $reviewId, isHelpful: $isHelpful) { ... }
}

# After — split per intent
mutation ReviewUpvote($reviewId: ID!) { reviewUpvote(reviewId: $reviewId) { ... } }
mutation ReviewDownvote($reviewId: ID!) { reviewDownvote(reviewId: $reviewId) { ... } }

Głosowanie jest idempotentne — ponowny upvote nadpisuje poprzedni głos tego samego klienta.

Stronger types: DateTime, enums, structured errors

Pola które poprzednio zwracały String (ISO 8601) lub untyped error arrays są teraz właściwie typed:

  • DateTime: Order.processedAt, BlogPost.{publishedAt,createdAt,updatedAt}, ShopPage.{publishedAt,createdAt,updatedAt}, LoyaltyMember.{lastActivityAt,enrolledAt}, LoyaltyTransaction.{createdAt,expiresAt}, LoyaltyPointsSummary.nextExpiryDate, CustomerAccessToken.expiresAt. Codegen emituje Date | string.
  • Enums: Customer.emailMarketingState jest EmailMarketingState!, ProductAttributeDefinition.fillingMode/billingMode typed enums, ProductAttributeOption.surchargeType jest AttributeOptionSurchargeType, Shop.currencyCode/paymentCurrencies/supportedCurrencies używają CurrencyCode, Shop.defaultLanguage/supportedLanguages używają LanguageCode, Currency.code jest CurrencyCode, Currency.symbolPosition jest nowym CurrencySymbolPosition enum (BEFORE/AFTER).
  • Structured errors: WishlistPayload.userErrors, RedeemRewardPayload.userErrors, GenerateReferralCodePayload.userErrors są teraz [UserError!]! ({ message, code, field }) zamiast [String!]!. Replace userErrors[0] z userErrors[0].message w error handlingu.

Co dalej

Pytania / problemy migracji: zgłoś issue w @doswiftly/storefront-sdk repo na GitHub.