Stage M5 and Stage M6
This commit is contained in:
@@ -3,6 +3,12 @@ import { createAdminResource, type AdminResource } from './resources/admin';
|
||||
import { createAuthResource, type AuthResource } from './resources/auth';
|
||||
import { createCatalogResource, type CatalogResource } from './resources/catalog';
|
||||
import { createCatalogAdminResource, type CatalogAdminResource } from './resources/catalog-admin';
|
||||
import {
|
||||
createCommerceResource,
|
||||
createOrdersAdminResource,
|
||||
type CommerceResource,
|
||||
type OrdersAdminResource,
|
||||
} from './resources/commerce';
|
||||
import { createHealthResource, type HealthResource } from './resources/health';
|
||||
|
||||
/**
|
||||
@@ -18,6 +24,8 @@ export interface ApiClient {
|
||||
readonly auth: AuthResource;
|
||||
readonly admin: AdminResource;
|
||||
readonly catalogAdmin: CatalogAdminResource;
|
||||
readonly commerce: CommerceResource;
|
||||
readonly ordersAdmin: OrdersAdminResource;
|
||||
}
|
||||
|
||||
export function createApiClient(options: HttpClientOptions): ApiClient {
|
||||
@@ -30,5 +38,7 @@ export function createApiClient(options: HttpClientOptions): ApiClient {
|
||||
auth: createAuthResource(http),
|
||||
admin: createAdminResource(http),
|
||||
catalogAdmin: createCatalogAdminResource(http),
|
||||
commerce: createCommerceResource(http),
|
||||
ordersAdmin: createOrdersAdminResource(http),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,4 +30,10 @@ export type {
|
||||
StockAdjustment,
|
||||
VariantPatch,
|
||||
} from './resources/catalog-admin';
|
||||
export type {
|
||||
CommerceResource,
|
||||
OrderListParams,
|
||||
OrdersAdminResource,
|
||||
PlaceOrderInput,
|
||||
} from './resources/commerce';
|
||||
export type { ApiClient } from './create-client';
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
Locale,
|
||||
NavigationMenu,
|
||||
ProductListResult,
|
||||
SearchSuggestions,
|
||||
StorefrontProduct,
|
||||
} from '@sport/types';
|
||||
|
||||
@@ -46,6 +47,16 @@ export interface CatalogResource {
|
||||
query?: ProductListQuery,
|
||||
options?: RequestOptions,
|
||||
): Promise<ProductListResult>;
|
||||
/**
|
||||
* Ranked search. Same result shape as a listing, so the same components
|
||||
* render it — only the ordering and the matching differ.
|
||||
*/
|
||||
searchProducts(
|
||||
locale: Locale,
|
||||
query?: ProductListQuery,
|
||||
options?: RequestOptions,
|
||||
): Promise<ProductListResult>;
|
||||
suggest(locale: Locale, query: string, options?: RequestOptions): Promise<SearchSuggestions>;
|
||||
getProduct(locale: Locale, slug: string, options?: RequestOptions): Promise<StorefrontProduct>;
|
||||
listProductSlugs(
|
||||
locale: Locale,
|
||||
@@ -79,6 +90,15 @@ export function createCatalogResource(http: HttpClient): CatalogResource {
|
||||
listProducts: (locale, query = {}, options) =>
|
||||
http.get<ProductListResult>('/products', withLocale(locale, { ...query }, options)),
|
||||
|
||||
searchProducts: (locale, query = {}, options) =>
|
||||
http.get<ProductListResult>('/search', withLocale(locale, { ...query }, options)),
|
||||
|
||||
suggest: (locale, query, options) =>
|
||||
http.get<SearchSuggestions>(
|
||||
'/search/suggestions',
|
||||
withLocale(locale, { q: query }, { ...options, cache: 'no-store' }),
|
||||
),
|
||||
|
||||
getProduct: (locale, slug, options) =>
|
||||
http.get<StorefrontProduct>(
|
||||
`/products/${encodeURIComponent(slug)}`,
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import type {
|
||||
Cart,
|
||||
Locale,
|
||||
OffsetPaginated,
|
||||
Order,
|
||||
OrderListItem,
|
||||
ShippingAddress,
|
||||
} from '@sport/types';
|
||||
|
||||
import type { HttpClient, RequestOptions } from '../http-client';
|
||||
|
||||
/**
|
||||
* Mirrors `placeOrderSchema` in @sport/validation.
|
||||
*
|
||||
* Declared here rather than imported because this package depends on
|
||||
* @sport/types alone — pulling in Zod would put a validation runtime into every
|
||||
* consumer, including a future React Native app. The server re-validates, so
|
||||
* this type is a convenience for callers and never the enforcement point.
|
||||
*/
|
||||
export interface PlaceOrderInput {
|
||||
email: string;
|
||||
shippingAddress: ShippingAddress;
|
||||
customerNote?: string | null;
|
||||
}
|
||||
|
||||
export interface OrderListParams {
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
q?: string;
|
||||
status?: 'PENDING' | 'CONFIRMED' | 'FULFILLED' | 'COMPLETED' | 'CANCELLED';
|
||||
}
|
||||
|
||||
/**
|
||||
* Cart, checkout and order access.
|
||||
*
|
||||
* Every cart call must send credentials: the bag is identified by an httpOnly
|
||||
* cookie, so a request without it silently starts a new, empty cart. The
|
||||
* `credentials` option below is the difference between "add to bag works" and
|
||||
* "add to bag appears to work".
|
||||
*/
|
||||
export interface CommerceResource {
|
||||
getCart(locale: Locale, options?: RequestOptions): Promise<Cart>;
|
||||
addCartLine(
|
||||
locale: Locale,
|
||||
input: { variantId: string; quantity?: number },
|
||||
options?: RequestOptions,
|
||||
): Promise<Cart>;
|
||||
updateCartLine(
|
||||
locale: Locale,
|
||||
variantId: string,
|
||||
quantity: number,
|
||||
options?: RequestOptions,
|
||||
): Promise<Cart>;
|
||||
removeCartLine(locale: Locale, variantId: string, options?: RequestOptions): Promise<Cart>;
|
||||
|
||||
getCheckoutQuote(locale: Locale, options?: RequestOptions): Promise<Cart>;
|
||||
placeOrder(locale: Locale, input: PlaceOrderInput, options?: RequestOptions): Promise<Order>;
|
||||
lookupOrder(orderNumber: string, email: string, options?: RequestOptions): Promise<Order>;
|
||||
getPlacedOrder(id: string, options?: RequestOptions): Promise<Order>;
|
||||
}
|
||||
|
||||
export interface OrdersAdminResource {
|
||||
listOrders(
|
||||
params?: OrderListParams,
|
||||
options?: RequestOptions,
|
||||
): Promise<OffsetPaginated<OrderListItem>>;
|
||||
getOrder(id: string, options?: RequestOptions): Promise<Order>;
|
||||
updateOrderStatus(
|
||||
id: string,
|
||||
status: OrderListParams['status'],
|
||||
reason?: string | null,
|
||||
options?: RequestOptions,
|
||||
): Promise<Order>;
|
||||
}
|
||||
|
||||
export function createCommerceResource(http: HttpClient): CommerceResource {
|
||||
/**
|
||||
* `credentials: 'include'` on every call, not just the mutations.
|
||||
*
|
||||
* The cart cookie is httpOnly and same-origin, and a GET without credentials
|
||||
* reads a different (empty) bag than the POST that filled it — a failure that
|
||||
* looks like "the cart randomly empties" rather than a missing option.
|
||||
*/
|
||||
const cartOptions = (locale: Locale, options?: RequestOptions): RequestOptions => ({
|
||||
...options,
|
||||
credentials: 'include',
|
||||
query: { locale, ...options?.query },
|
||||
// A bag is per-visitor state; caching it anywhere would serve someone
|
||||
// else's.
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
return {
|
||||
getCart: (locale, options) => http.get<Cart>('/cart', cartOptions(locale, options)),
|
||||
|
||||
addCartLine: (locale, input, options) =>
|
||||
http.post<Cart>('/cart/lines', input, cartOptions(locale, options)),
|
||||
|
||||
updateCartLine: (locale, variantId, quantity, options) =>
|
||||
http.patch<Cart>(
|
||||
`/cart/lines/${encodeURIComponent(variantId)}`,
|
||||
{ quantity },
|
||||
cartOptions(locale, options),
|
||||
),
|
||||
|
||||
removeCartLine: (locale, variantId, options) =>
|
||||
http.delete<Cart>(
|
||||
`/cart/lines/${encodeURIComponent(variantId)}`,
|
||||
cartOptions(locale, options),
|
||||
),
|
||||
|
||||
getCheckoutQuote: (locale, options) =>
|
||||
http.get<Cart>('/checkout/quote', cartOptions(locale, options)),
|
||||
|
||||
placeOrder: (locale, input, options) =>
|
||||
http.post<Order>('/checkout/orders', input, cartOptions(locale, options)),
|
||||
|
||||
getPlacedOrder: (id, options) =>
|
||||
http.get<Order>(`/checkout/orders/${encodeURIComponent(id)}`, {
|
||||
...options,
|
||||
cache: 'no-store',
|
||||
}),
|
||||
|
||||
lookupOrder: (orderNumber, email, options) =>
|
||||
http.get<Order>('/checkout/orders/lookup', {
|
||||
...options,
|
||||
query: { orderNumber, email },
|
||||
cache: 'no-store',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function createOrdersAdminResource(http: HttpClient): OrdersAdminResource {
|
||||
return {
|
||||
listOrders: (params = {}, options) =>
|
||||
http.get<OffsetPaginated<OrderListItem>>('/admin/orders', {
|
||||
...options,
|
||||
query: { ...params, ...options?.query },
|
||||
}),
|
||||
|
||||
getOrder: (id, options) => http.get<Order>(`/admin/orders/${encodeURIComponent(id)}`, options),
|
||||
|
||||
updateOrderStatus: (id, status, reason, options) =>
|
||||
http.patch<Order>(
|
||||
`/admin/orders/${encodeURIComponent(id)}/status`,
|
||||
{ status, reason: reason ?? null },
|
||||
options,
|
||||
),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user