Zamówienia
Kompletny przewodnik po Order GraphQL surface w storefroncie: zalogowany dostęp do własnego zamówienia, guest order summary po checkout bez konta, top-level Order.lineItems z gwarantowaną ochroną przed N+1.
Dostępne operacje
Zapytania
| Operation | Auth | Use case |
|---|---|---|
CustomerOrder($orderId) | wymaga sesji (cookie/Bearer) | Szczegóły jednego zamówienia dla zalogowanego klienta |
OrderByToken($token, $email?) | bez auth | Guest order summary po cartComplete (token z cartComplete.order.accessToken) |
Pola Order
Po upgradzie @doswiftly/storefront-operations do najnowszej wersji minor, fragment Order zawiera:
accessToken: String!— opaque per-order token używany jako klucz doOrderByTokenquery. Zwracany wcartComplete.order.accessTokennatychmiast po checkout.lineItems(first: Int = 10, after: String): OrderLineItemConnection!— Relay-style connection z line items (top-level, NIE wymaga fetcha shipments).customerNote: String— notatka, którą kupujący zostawił przy składaniu zamówienia (instrukcje dostawy, wiadomość prezentowa itp.); odzwierciedla notatkę ustawioną na koszyku przezCartUpdateNote.null, gdy notatki nie podano.- Standardowe:
id,orderNumber,status,totals { total, subtotal, totalTax, totalShipping },paymentStatus,fulfillmentStatus,processedAt,shippingAddress,itemCount,canCreatePayment,paymentMethodType.
Import
// CustomerOrder (zalogowany klient) — client hook
import { useCustomerOrder } from '@/lib/graphql/hooks';
OrderByToken (guest order po tokenie) nie ma template-hooka — to operacja raw-only. Użyj jej przez raw operation (poniżej <RenderingTabs operation="OrderByToken" />) albo metodę SDK CartClient.getOrderByToken(token, email?).
- Raw (dowolny framework)
Brak gotowego helpera SDK dla tej operacji — użyj raw operation (działa w każdym frameworku).
// Działa w dowolnym frameworku (Vue, Svelte, vanilla JS, Node, Edge)
const QUERY = `query OrderByToken($token: String!, $email: String) {
orderByToken(token: $token, email: $email) {
...Order
}
}
fragment Order on Order {
id
orderNumber
accessToken
totals {
total {
...Money
}
subtotal {
...Money
}
totalTax {
...Money
}
totalShipping {
...Money
}
feeTotal {
...Money
}
feeAllocations {
label
amount {
...Money
}
}
pricesIncludeTax
}
status
paymentStatus
fulfillmentStatus
processedAt
confirmedAt
cancelledAt
expiredAt
shippingAddress {
...MailingAddress
}
itemCount
customerNote
discountAllocations {
discountCode
amount {
...Money
}
}
canCreatePayment
paymentMethodType
bankTransferInstructions {
bankName
accountNumber
accountHolder
transferTitle
amount {
...Money
}
}
}
fragment MailingAddress on MailingAddress {
id
streetLine1
streetLine2
buildingNumber
flatNumber
city
company
country
countryCode
firstName
lastName
name
phone
state
stateCode
postalCode
isDefault
taxId
vatNumber
regon
pickupPoint {
...PickupPoint
}
}
fragment PickupPoint on PickupPoint {
provider
pointId
name
address
paymentAvailable
}
fragment Money on Money {
amount
currencyCode
}`;
const res = await fetch(`${apiUrl}/storefront/graphql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: QUERY, variables: { token: '…', email: '…' }, }),
});
const { data } = await res.json();
Typed dokumenty codegen pochodzą z @doswiftly/storefront-operations. Po upgradzie SDK i operations uruchom pnpm run codegen.
Guest order summary po checkout
Problem: gość kończy checkout, twój storefront chce pokazać podsumowanie zamówienia od razu — bez zakładania konta i bez czekania na e-mail. customerOrder(orderId) wymaga sesji, więc dla gościa zwraca null.
Rozwiązanie: cartComplete zwraca Order.accessToken (opaque UUID). Storefront persistuje token, redirectuje na stronę summary, używa OrderByToken query.
End-to-end flow
'use client';
import { useRouter } from 'next/navigation';
import { useCartManager } from '@doswiftly/storefront-sdk/react';
// `complete()` jest częścią pełnego lifecycle koszyka w useCartManager — patrz [Koszyk](./cart.md).
export function CheckoutButton() {
const router = useRouter();
const { complete, status } = useCartManager();
return (
<button
disabled={status.isLoading}
onClick={async () => {
const { order } = await complete();
if (!order) return;
// Token w URL — server-side render summary
router.push(`/order/summary?token=${order.accessToken}`);
}}
>
Złóż zamówienie
</button>
);
}
Strona summary (Server Component preferowane)
// app/order/summary/page.tsx
import { getStorefrontClient } from '@doswiftly/storefront-sdk/react/server';
import { CartClient } from '@doswiftly/storefront-sdk';
import { OrderSummaryView } from '@/components/order/summary-view';
export default async function OrderSummaryPage({
searchParams,
}: {
searchParams: { token?: string };
}) {
const token = searchParams.token;
if (!token) return <p>Brak tokenu — link nieprawidłowy.</p>;
// OrderByToken nie ma template-hooka — to metoda SDK CartClient (działa server-side)
const order = await new CartClient(getStorefrontClient()).getOrderByToken(token);
if (!order) {
return (
<p>
Nie znaleziono zamówienia. Sprawdź skrzynkę e-mail — wysłaliśmy
potwierdzenie z linkiem do podsumowania.
</p>
);
}
return <OrderSummaryView order={order} />;
}
Token storage best-practices
Token jest opaque secret: kto go ma, ten widzi szczegóły zamówienia (numer, kwota, lista produktów, adres dostawy). Musi być chroniony przed XSS i shared device leakage.
Rekomendowane
| Storage | Use case | Trade-off |
|---|---|---|
HTTP-only cookie (cart-id-style, sameSite=lax, secure=true) | Domyślny dla guest summary, działa SSR + edge | Niewidoczny dla JS = bezpieczny przed XSS; wymaga server-side cookie write w callbacku po cartComplete |
sessionStorage | Storefront SPA bez SSR, gdy cookie ustawienie niepraktyczne | Czyszczony po zamknięciu tab; chroni przed shared device, ale czytelny dla JS (XSS risk) |
URL query parameter (?token=...) | Tymczasowo w trakcie redirect cartComplete → /order/summary | Wycieka do server logs, browser history, Referer header — NIE persistuj URL po render, pobierz token z params i renderuj summary bez retencji w URL |
NIGDY
localStorage— czytelny dla XSS w storefroncie. Token może być wykradziony przez injected script.- Trzymanie tokenu w global state Redux/Zustand bez czyszczenia po render summary — w shared browser kolejny user zobaczy.
Defense-in-depth: opcjonalny email guard
OrderByToken przyjmuje opcjonalny parametr email. Gdy podany, backend porównuje case-insensitive z buyer email zamówienia. Mismatch zwraca null identycznie jak invalid token — atakujący nie odróżni "token poprawny ale email zły" od "token zły".
Kiedy warto użyć email guard
| Scenariusz | Z email guard | Bez guard (token-only) |
|---|---|---|
| Token wyciekł do server logs / Referer | Logi nie zawierają emaila → atakujący ma niekompletne dane | Atakujący ma pełny dostęp |
| Shared device / kiosk z browser history | Następny user widzi URL bez emaila → mismatch → null | Następny user widzi order |
| Storefront ma context emaila z checkout (sessionStorage) | Łatwy do podpięcia bez friction | n/a |
const cart = new CartClient(getStorefrontClient());
// Wariant z email guard — preferowany jeśli storefront trzyma kontekst checkout
const order = await cart.getOrderByToken(tokenFromUrl, emailFromCheckoutSession);
// Wariant token-only — prostszy, gdy nie masz wygodnego dostępu do email
const order = await cart.getOrderByToken(tokenFromUrl);
Top-level Order.lineItems (eliminuje N+1)
Order.lineItems to Relay-style connection na top-levelu, eliminujący wcześniejszą konieczność fetchowania shipments[].items[] per order.
Order listing — preview produktów per zamówienie
query CustomerOrdersList {
customer {
id
orders(first: 20) {
nodes {
id
orderNumber
totals { total { amount currencyCode } }
lineItems(first: 3) {
nodes {
id
title
quantity
variant { image { url } }
}
}
}
}
}
}
Backend wykonuje stałą liczbę zapytań SQL niezależnie od liczby orderów na listingu (backend pobiera w stałej liczbie zapytań, bez N+1). Wcześniejsze podejście (fetch shipments[].items[] per order) generowało N+1 — 20 orderów × 1 dodatkowy query = 20 SQL hits.
Pola OrderLineItem
| Pole | Typ | Semantyka |
|---|---|---|
id | ID! | Stable identifier |
title | String! | Nazwa produktu z momentu zakupu (snapshot — NIE live Product.name). Preserved nawet po edycji produktu. |
quantity | Int! | Ilość zamówiona |
variant | ProductVariant (nullable) | Live wariant produktu — null gdy wariant został usunięty po zakupie. Storefront powinien fallback'ować na title. |
originalUnitPrice | Money! | Cena jednostkowa z momentu checkout (final, z BUNDLED surcharges) |
originalTotalPrice | Money! | Całkowita cena linii (unit × quantity) |
quantityFulfilled | Int! | Ile już wysłane (per-item fulfillment) |
refundableQuantity | Int! | Pozostała ilość dostępna do zwrotu (quantity - returnedQuantity, clamp ≥ 0) |
Variant deleted handling
{order.lineItems.nodes.map((item) => (
<div key={item.id}>
{item.variant?.image ? (
<img src={item.variant.image.url} alt={item.title} />
) : (
// Fallback gdy wariant został usunięty po zakupie
<DefaultProductImage />
)}
<span>{item.title}</span>
<span>x{item.quantity}</span>
</div>
))}
Bezpieczeństwo
OrderByToken jest chroniony wielowarstwowo:
| Warstwa | Mechanizm |
|---|---|
| Rate-limit | 5 zapytań / minuta per IP+shop. Przekroczenie → GraphQL error z extensions.code: THROTTLED. Chroni przed token enumeration. |
| Email guard (opcjonalny) | Patrz sekcja powyżej. Storefront-dev decides per threat model. |
| Uniform null returns | Invalid token, email mismatch, not found = ten sam null. Atakujący nie wnioskuje "token poprawny ale email zły". |
Cache-Control: no-store | Response nie cache'owany w CDN/proxy/browser. Per-customer data nigdy nie serwowane między userami. |
| Izolacja między sklepami | Dane każdego sklepu są w pełni izolowane — token ze sklepu A nigdy nie zwróci zamówienia sklepu B. |
Obsługa rate-limit po stronie storefrontu
import { CartClient, StorefrontError } from '@doswiftly/storefront-sdk';
// getOrderByToken rzuca StorefrontError gdy backend zwróci błąd (np. rate-limit THROTTLED).
// Uwaga: throttling przychodzi jako błąd GraphQL — `err.code` to 'GRAPHQL_ERROR',
// a sygnał THROTTLED znajdziesz w `extensions.code` błędu GraphQL.
async function loadOrderSummary(cart: CartClient, token: string) {
try {
const order = await cart.getOrderByToken(token);
if (!order) return { kind: 'not-found' as const };
return { kind: 'ok' as const, order };
} catch (err) {
if (
err instanceof StorefrontError &&
err.graphqlErrors.some((e) => e.extensions?.code === 'THROTTLED')
) {
// Zbyt wiele prób w krótkim czasie — pokaż komunikat, poproś o sprawdzenie e-maila
return { kind: 'throttled' as const };
}
throw err;
}
}
Migration z 11.x
Brak breaking changes. Po upgradzie @doswiftly/storefront-operations + @doswiftly/storefront-sdk do najnowszej wersji minor:
- Wszystkie istniejące queries (
CustomerOrder, cart mutations, customer auth) działają bez zmian. - Nowe pola/queries są opcjonalne — storefront opt-in kiedy potrzebuje.
- Bezpieczny upgrade: bump wersji w
package.json,pnpm install,pnpm run codegen(regenerate typed documents) — storefront działa identycznie. Następnie opcjonalnie dodajaccessTokendo swojego fragmentu Order i wprowadźOrderByTokenquery gdzie potrzebujesz guest summary.
Krok po kroku
-
Bump wersji w
package.json:{
"dependencies": {
"@doswiftly/storefront-operations": "^X.Y.0",
"@doswiftly/storefront-sdk": "^X.Y.0"
}
}(synchronizacja wersji wymagana — paczki są linked).
-
Regenerate codegen:
pnpm install
pnpm run codegen -
Opcjonalnie — wpięcie guest summary (przykłady wyżej).
Powiązane
@doswiftly/storefront-operationsREADME — pełna lista queries/mutations/fragments@doswiftly/storefront-sdkREADME — CartClient, AuthClient, providers- Cart i checkout — flow przed
cartComplete - Konto klienta —
customer.orderslisting dla zalogowanych