Files
vmall/apps/mall/plugins/api.ts
T
james 104737e4e1 feat(mall): serve home marketing content from the API
Wave 4 of replacing the fixed-data mock adapter, and the first capability the
mall never had a backend for: the home page's banners, promo tiles, quick links
and floor advert art move out of local arrays.

- four explicit tables (`banners`, `promos`, `quick_links`, `floor_adverts`)
  rather than one JSONB payload table, so Postgres enforces each shape
- a migration seeds them from the assets the page already rendered, so the flip
  is visually a no-op. Destinations are real routes now: the mock's promo links
  pointed at dangling `?category=c1` ids and its first banner used `sort=sales`,
  which the catalog API rejects
- `GET /api/content/home` is public and returns the four active, ordered lists,
  always including a key so a page can render a missing block
- `GET /api/admin/content` and `PUT /api/admin/content/{kind}` let a platform
  admin read everything and replace one kind transactionally, with positions
  reindexed from the submitted order and a rejected list changing nothing
- the mall's fixed-data adapter learns `getHomeContent`, and a `content` domain
  joins the per-domain switch so the rollback path still renders the page

Verified: 23 backend tests green including six new content tests; all three
frontends build; the home page renders the same four blocks as before, an admin
reorder and deactivation change the rendered carousel, and the fixed-data
rollback renders every block with the backend stopped.

OpenSpec change: openspec/changes/replace-mock-api-wave-4
2026-09-17 16:48:10 +00:00

93 lines
2.8 KiB
TypeScript

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"
| "content"
| "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 }),
content: (a: ApiClient) => ({ getHomeContent: a.getHomeContent }),
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[];
/** Fallback when runtimeConfig supplies no list; keep in step with nuxt.config.ts. */
const DEFAULT_LIVE_DOMAINS: LiveDomain[] = [
"catalog",
"currency",
"content",
"auth",
"cart",
"orders",
"shipments",
"invoices",
];
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig();
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 } };
});