feat(mall): serve catalog and currency from the live API
Wave 1 of replacing the fixed-data mock adapter. The mall now selects its API adapter per domain, with catalog and currency served live while auth, cart, orders, shipments and invoices stay on fixed data. Backend: - seed the 6 x 2 x 2 category tree as reference data (migration 0005). The API exposes no category write route, so this cannot come from the seed script - filter public product listing by the category subtree with a recursive CTE, matching the mock's existing behaviour instead of exact-match - add sort=price with order=asc|desc, validated by hand so an unsupported value returns the project's ApiError 400 shape rather than axum's own rejection Mall: - replace the all-or-nothing mockApi boolean with a liveDomains list composed through a typed per-domain pick map - source home floors, the category menu, search and product detail from the catalog API; banners, promos, quick links, store card and comment/coupon content stay local display-only content - drop the brand facet and the sales/comments sorts: no backend model backs them - fix salesOf/commentCountOf, which parsed digits out of the product id and so rendered "NaN sold" for live UUID ids; they now hash the id Seed: 24 products across 4 shops, idempotent on re-run. Note: the mall defaults to a live catalog, so pnpm dev:mall now expects the API to be running; set NUXT_PUBLIC_LIVE_DOMAINS to an empty array for all-mock work. OpenSpec change: openspec/changes/replace-mock-api-wave-1
This commit is contained in:
@@ -1,16 +1,74 @@
|
||||
import { createApi } from "@vmall/shared";
|
||||
import type { ApiClient } from "@vmall/shared";
|
||||
import { createMockApi } from "~/mock/api";
|
||||
|
||||
/**
|
||||
* Domains the live backend serves. Every other domain stays on the fixed-data
|
||||
* adapter, so a domain can be migrated - or rolled back - by editing this list
|
||||
* alone. See openspec/changes/replace-mock-api-wave-1/design.md.
|
||||
*/
|
||||
type LiveDomain = "auth" | "catalog" | "currency" | "cart" | "orders" | "shipments" | "invoices";
|
||||
|
||||
/**
|
||||
* Explicit per-domain method picks rather than a string allowlist: indexing
|
||||
* `ApiClient` by a union of method names would need an unsafe cast, and this
|
||||
* keeps the compiler checking that every picked key exists.
|
||||
*/
|
||||
const LIVE_PICKS = {
|
||||
auth: (a: ApiClient) => ({ register: a.register, login: a.login, me: a.me }),
|
||||
catalog: (a: ApiClient) => ({
|
||||
listProducts: a.listProducts,
|
||||
getProduct: a.getProduct,
|
||||
listCategories: a.listCategories,
|
||||
}),
|
||||
currency: (a: ApiClient) => ({ listCurrencies: a.listCurrencies, convert: a.convert }),
|
||||
cart: (a: ApiClient) => ({
|
||||
getCart: a.getCart,
|
||||
addCartItem: a.addCartItem,
|
||||
updateCartItem: a.updateCartItem,
|
||||
removeCartItem: a.removeCartItem,
|
||||
}),
|
||||
orders: (a: ApiClient) => ({
|
||||
checkout: a.checkout,
|
||||
listMyOrders: a.listMyOrders,
|
||||
getOrder: a.getOrder,
|
||||
cancelOrder: a.cancelOrder,
|
||||
payOrder: a.payOrder,
|
||||
}),
|
||||
shipments: (a: ApiClient) => ({
|
||||
confirmDelivered: a.confirmDelivered,
|
||||
listMyShipments: a.listMyShipments,
|
||||
}),
|
||||
invoices: (a: ApiClient) => ({
|
||||
requestInvoice: a.requestInvoice,
|
||||
listMyInvoices: a.listMyInvoices,
|
||||
}),
|
||||
} satisfies Record<LiveDomain, (a: ApiClient) => Partial<ApiClient>>;
|
||||
|
||||
const KNOWN_DOMAINS = Object.keys(LIVE_PICKS) as LiveDomain[];
|
||||
|
||||
/** Wave 1: browse and price display come from the backend, everything else stays fixed-data. */
|
||||
const DEFAULT_LIVE_DOMAINS: LiveDomain[] = ["catalog", "currency"];
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
const config = useRuntimeConfig();
|
||||
// Mock-first MVP: default to the fixed-data adapter; set NUXT_PUBLIC_MOCK_API=false
|
||||
// (or runtimeConfig.public.mockApi) to talk to the live backend again.
|
||||
const useMock = (config.public.mockApi as boolean | undefined) !== false;
|
||||
const api = useMock
|
||||
? createMockApi()
|
||||
: createApi({
|
||||
baseUrl: config.public.apiBase as string,
|
||||
getToken: () => (import.meta.client ? localStorage.getItem("vmall.token") : null),
|
||||
});
|
||||
const mock = createMockApi();
|
||||
const live = createApi({
|
||||
baseUrl: config.public.apiBase as string,
|
||||
getToken: () => (import.meta.client ? localStorage.getItem("vmall.token") : null),
|
||||
});
|
||||
|
||||
const configured = config.public.liveDomains as string[] | undefined;
|
||||
const liveDomains = (configured ?? DEFAULT_LIVE_DOMAINS).filter((domain): domain is LiveDomain =>
|
||||
KNOWN_DOMAINS.includes(domain as LiveDomain),
|
||||
);
|
||||
|
||||
// The fixed-data adapter is the base object, so an unmigrated domain cannot
|
||||
// regress and a live domain can be rolled back by removing one entry.
|
||||
let api: ApiClient = mock;
|
||||
for (const domain of liveDomains) {
|
||||
api = { ...api, ...LIVE_PICKS[domain](live) };
|
||||
}
|
||||
|
||||
return { provide: { api } };
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user