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.
- Zaktualizuj
@doswiftly/cli,@doswiftly/storefront-sdk,@doswiftly/storefront-operationsdo^5.0.0 - Uruchom codegen lokalnie — TypeScript wymusi większość renames przez compile errors
- Zaktualizuj queries/mutations według tabel poniżej
- Zaktualizuj komponenty czytające addresses (US/CA → international naming)
- Zaktualizuj UI używające
cart.subtotalPrice/order.totalPricena nestedcost.subtotal/totals.total - Sprawdź auth flow — token przechowywany teraz w httpOnly cookie, nie localStorage
Spis treści
- Authentication: cookie-first
- Adresy: international naming
- Costs/Totals: nested wrappers
- Cart line: variant zamiast Merchandise union
- UserError: jeden generic typ
- Wishlist + GiftCard: pagination + simplified read
- Recommendations: field resolvers zamiast top-level queries
- Filter scalars + Weight type
- Naming sweep: mutations + fields
- Stronger types: DateTime, enums, structured errors
- Co dalej
Authentication: cookie-first
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) |
|---|---|
address1 | streetLine1 |
address2 | streetLine2 |
province | state |
provinceCode | stateCode |
zip | postalCode |
# 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:
subtotalAmountWithConversion→subtotalWithConversiontotalAmountWithConversion→totalWithConversionamountPerQuantityWithConversion→pricePerUnitWithConversion
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 / BaseCartLineEdge → CartLineConnection / CartLineEdge.
Cart line input renamed merchandiseId → variantId:
# 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: customerUserErrors → userErrors (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
| Przed | Po |
|---|---|
cartLinesAdd | cartAddLines |
cartLinesUpdate | cartUpdateLines |
cartLinesRemove | cartRemoveLines |
cartNoteUpdate | cartUpdateNote |
cartAttributesUpdate | cartUpdateAttributes |
cartBuyerIdentityUpdate | cartUpdateBuyerIdentity |
cartDiscountCodesUpdate | cartApplyDiscountCodes |
Resource-id arg renamed cartId → id na mutations (foreign-key CheckoutCreateInput.cartId zachowuje nazwę).
Customer mutations
| Przed | Po |
|---|---|
customerAccessTokenCreate | customerLogin |
customerAccessTokenDelete | customerLogout |
customerAccessTokenRenew | customerRefreshToken |
customerCreate | customerSignup |
customerRecover | customerRequestPasswordReset |
customerResetByUrl | customerResetPassword |
customerActivateByUrl | customerActivate |
customerAddressCreate | customerAddAddress |
customerAddressUpdate | customerUpdateAddress |
customerAddressDelete | customerRemoveAddress |
customerDefaultAddressUpdate | customerSetDefaultAddress |
Checkout mutations
| Przed | Po |
|---|---|
checkoutShippingAddressUpdate | checkoutUpdateShippingAddress |
checkoutBillingAddressUpdate | checkoutUpdateBillingAddress |
checkoutEmailUpdate | checkoutUpdateEmail |
checkoutShippingLineUpdate | checkoutSelectShippingRate (+ arg shippingRateHandle → rateId) |
checkoutDiscountCodeApply | checkoutApplyDiscountCode |
checkoutDiscountCodeRemove | checkoutRemoveDiscountCode |
checkoutGiftCardApply | checkoutApplyGiftCard |
checkoutPaymentMethodUpdate | checkoutSelectPaymentMethod |
Top-level query renames
| Przed | Po |
|---|---|
recommendations | personalizedRecommendations |
predictiveSearch | searchSuggestions |
productSearch | searchProducts |
availableFilters | productFilters |
Field renames na output types
| Type.field | Po |
|---|---|
Product.productType | Product.category (free-text classification; Product.type: ProductTypeEnum enum field bez zmian) |
Product.totalInventory | Product.stockTotal |
Product.collectRecipientInfo | usunięte (storefront-driven recipient mode — patrz Karty podarunkowe w cart.md) |
ProductVariant.quantityAvailable | availableStock |
ProductOption.position / ProductOptionValue.position | sortOrder |
Customer.numberOfOrders | orderCount |
Customer.emailMarketingState | Customer.emailMarketing (typed enum) |
WishlistItem.priceAtAdd | priceWhenAdded |
Shop.primaryDomain.sslEnabled | isSslEnabled (boolean prefix) |
Shipment.estimatedDeliveryDate | estimatedDeliveryAt |
StoreAvailability.pickUpTime | pickupTime |
Location.pickupEnabled | supportsPickup |
ShopPage.bodySummary | excerpt |
BlogPost.contentType | BlogPost.contentFormat (typed enum HTML/MARKDOWN) |
BlogPost.readingTime | readingTimeMinutes |
Order.financialStatus | paymentStatus |
AvailableFilters.activeFilterCount | activeCount |
AvailableFilters.totalProducts | matchCount |
Menu.itemsCount | usunięte (= items.length) |
MenuItem.url | typed URL scalar (było String) |
LoyaltyReward.remainingRedemptions | redemptionsRemaining |
LoyaltyPageInfo | usunięte (użyj PageInfo) |
Shop.primaryDomain (string) + primaryDomainObject (Domain) | primaryDomain: Domain! (jeden field) |
GiftCard.lastCharacters | usunięte (compute z maskedCode.slice(-4)) |
Boolean prefix is/has/can/supports
Wszędzie boolean fields → is* / has* / supports*:
Product.availableForSale→Product.isAvailableProductVariant.availableForSale→ProductVariant.isAvailableProduct.purchasable→isPurchasableCustomer.emailVerified→isEmailVerifiedCheckout.{ready,completed}→{isReady,isCompleted}Discount*.applicable→isApplicable*Validation.valid→isValidLocation.pickupEnabled→supportsPickupStoreAvailability.available→isAvailableLoyaltyReward.available→isAvailable
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 emitujeDate | string. - Enums:
Customer.emailMarketingStatejestEmailMarketingState!,ProductAttributeDefinition.fillingMode/billingModetyped enums,ProductAttributeOption.surchargeTypejestAttributeOptionSurchargeType,Shop.currencyCode/paymentCurrencies/supportedCurrenciesużywająCurrencyCode,Shop.defaultLanguage/supportedLanguagesużywająLanguageCode,Currency.codejestCurrencyCode,Currency.symbolPositionjest nowymCurrencySymbolPositionenum (BEFORE/AFTER). - Structured errors:
WishlistPayload.userErrors,RedeemRewardPayload.userErrors,GenerateReferralCodePayload.userErrorssą teraz[UserError!]!({ message, code, field }) zamiast[String!]!. ReplaceuserErrors[0]zuserErrors[0].messagew error handlingu.
Co dalej
- CHANGELOG: pełna lista zmian per wersja w
node_modules/@doswiftly/storefront-sdk/CHANGELOG.md(oraz storefront-operations + cli) - Reference: API queries, API mutations, API types
- SDK: SDK overview, Customer auth
Pytania / problemy migracji: zgłoś issue w @doswiftly/storefront-sdk repo na GitHub.