Stage M1
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { HttpClient, type HttpClientOptions } from './http-client';
|
||||
import { createCatalogResource, type CatalogResource } from './resources/catalog';
|
||||
import { createHealthResource, type HealthResource } from './resources/health';
|
||||
|
||||
/**
|
||||
@@ -10,6 +11,7 @@ import { createHealthResource, type HealthResource } from './resources/health';
|
||||
export interface ApiClient {
|
||||
readonly http: HttpClient;
|
||||
readonly health: HealthResource;
|
||||
readonly catalog: CatalogResource;
|
||||
}
|
||||
|
||||
export function createApiClient(options: HttpClientOptions): ApiClient {
|
||||
@@ -18,5 +20,6 @@ export function createApiClient(options: HttpClientOptions): ApiClient {
|
||||
return {
|
||||
http,
|
||||
health: createHealthResource(http),
|
||||
catalog: createCatalogResource(http),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,4 +15,5 @@ export { ApiClientError, isApiClientError } from './errors';
|
||||
export { HttpClient } from './http-client';
|
||||
export type { HttpClientOptions, RequestOptions } from './http-client';
|
||||
export { createApiClient } from './create-client';
|
||||
export type { CatalogResource, ProductListQuery } from './resources/catalog';
|
||||
export type { ApiClient } from './create-client';
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import type {
|
||||
Brand,
|
||||
Category,
|
||||
CategoryNode,
|
||||
Collection,
|
||||
Locale,
|
||||
NavigationMenu,
|
||||
ProductListResult,
|
||||
StorefrontProduct,
|
||||
} from '@sport/types';
|
||||
|
||||
import type { HttpClient, RequestOptions } from '../http-client';
|
||||
|
||||
/**
|
||||
* Filters accepted by the listing endpoint. Mirrors `productFilterSchema` in
|
||||
* @sport/validation — the server re-validates, so this type is a convenience
|
||||
* for callers, never the enforcement point.
|
||||
*/
|
||||
export interface ProductListQuery {
|
||||
q?: string;
|
||||
categorySlug?: string;
|
||||
collectionSlug?: string;
|
||||
brandSlugs?: readonly string[];
|
||||
gender?: readonly string[];
|
||||
sport?: readonly string[];
|
||||
colors?: readonly string[];
|
||||
sizes?: readonly string[];
|
||||
minPrice?: number;
|
||||
maxPrice?: number;
|
||||
onSale?: boolean;
|
||||
inStockOnly?: boolean;
|
||||
sort?: 'newest' | 'price_asc' | 'price_desc' | 'best_selling' | 'relevance';
|
||||
cursor?: string | null;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache policy is the *caller's* decision, not this package's — a listing on
|
||||
* the homepage and the same listing behind a filter want different revalidation.
|
||||
* Every method therefore accepts RequestOptions and applies no default beyond
|
||||
* what the endpoint semantically requires.
|
||||
*/
|
||||
export interface CatalogResource {
|
||||
listProducts(
|
||||
locale: Locale,
|
||||
query?: ProductListQuery,
|
||||
options?: RequestOptions,
|
||||
): Promise<ProductListResult>;
|
||||
getProduct(locale: Locale, slug: string, options?: RequestOptions): Promise<StorefrontProduct>;
|
||||
listProductSlugs(
|
||||
locale: Locale,
|
||||
options?: RequestOptions,
|
||||
): Promise<{ slug: string; updatedAt: string }[]>;
|
||||
|
||||
listBrands(locale: Locale, options?: RequestOptions): Promise<Brand[]>;
|
||||
getBrand(locale: Locale, slug: string, options?: RequestOptions): Promise<Brand>;
|
||||
|
||||
getCategoryTree(locale: Locale, options?: RequestOptions): Promise<CategoryNode[]>;
|
||||
getCategory(locale: Locale, slug: string, options?: RequestOptions): Promise<Category>;
|
||||
|
||||
listCollections(locale: Locale, options?: RequestOptions): Promise<Collection[]>;
|
||||
getCollection(locale: Locale, slug: string, options?: RequestOptions): Promise<Collection>;
|
||||
|
||||
getNavigation(locale: Locale, options?: RequestOptions): Promise<NavigationMenu>;
|
||||
}
|
||||
|
||||
export function createCatalogResource(http: HttpClient): CatalogResource {
|
||||
/** Locale rides along on every catalog request; callers never forget it. */
|
||||
const withLocale = (
|
||||
locale: Locale,
|
||||
query: Record<string, unknown> = {},
|
||||
options?: RequestOptions,
|
||||
): RequestOptions => ({
|
||||
...options,
|
||||
query: { locale, ...(query as RequestOptions['query']), ...options?.query },
|
||||
});
|
||||
|
||||
return {
|
||||
listProducts: (locale, query = {}, options) =>
|
||||
http.get<ProductListResult>('/products', withLocale(locale, { ...query }, options)),
|
||||
|
||||
getProduct: (locale, slug, options) =>
|
||||
http.get<StorefrontProduct>(
|
||||
`/products/${encodeURIComponent(slug)}`,
|
||||
withLocale(locale, {}, options),
|
||||
),
|
||||
|
||||
listProductSlugs: (locale, options) =>
|
||||
http.get<{ slug: string; updatedAt: string }[]>(
|
||||
'/products/slugs',
|
||||
withLocale(locale, {}, options),
|
||||
),
|
||||
|
||||
listBrands: (locale, options) => http.get<Brand[]>('/brands', withLocale(locale, {}, options)),
|
||||
|
||||
getBrand: (locale, slug, options) =>
|
||||
http.get<Brand>(`/brands/${encodeURIComponent(slug)}`, withLocale(locale, {}, options)),
|
||||
|
||||
getCategoryTree: (locale, options) =>
|
||||
http.get<CategoryNode[]>('/categories', withLocale(locale, {}, options)),
|
||||
|
||||
getCategory: (locale, slug, options) =>
|
||||
http.get<Category>(
|
||||
`/categories/${encodeURIComponent(slug)}`,
|
||||
withLocale(locale, {}, options),
|
||||
),
|
||||
|
||||
listCollections: (locale, options) =>
|
||||
http.get<Collection[]>('/collections', withLocale(locale, {}, options)),
|
||||
|
||||
getCollection: (locale, slug, options) =>
|
||||
http.get<Collection>(
|
||||
`/collections/${encodeURIComponent(slug)}`,
|
||||
withLocale(locale, {}, options),
|
||||
),
|
||||
|
||||
getNavigation: (locale, options) =>
|
||||
http.get<NavigationMenu>('/navigation', withLocale(locale, {}, options)),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user