mirror of
https://github.com/vercel/commerce.git
synced 2025-05-14 13:47:49 +00:00
336 lines
11 KiB
TypeScript
336 lines
11 KiB
TypeScript
import { headers } from 'next/headers';
|
|
import { NextRequest, NextResponse } from 'next/server';
|
|
import { revalidateTag } from 'next/cache';
|
|
import { TAGS } from 'lib/constants';
|
|
import {
|
|
requestCategory,
|
|
requestCategoryList,
|
|
requestCategoryProductsCollection,
|
|
requestCrossSell,
|
|
requestNavigation,
|
|
requestProductsCollection,
|
|
requestSearchCollectionProducts,
|
|
requestSeoUrl,
|
|
requestSeoUrls
|
|
} from './api';
|
|
import { ExtendedCategory, ExtendedProduct, ExtendedProductListingResult } from './api-extended';
|
|
import {
|
|
getDefaultCategoryCriteria,
|
|
getDefaultCategoryWithCmsCriteria,
|
|
getDefaultCrossSellingCriteria,
|
|
getDefaultProductCriteria,
|
|
getDefaultProductsCriteria,
|
|
getDefaultSearchProductsCriteria,
|
|
getDefaultSubCategoriesCriteria,
|
|
getSeoUrlCriteria,
|
|
getSortingCriteria
|
|
} from './criteria';
|
|
import {
|
|
transformCollection,
|
|
transformHandle,
|
|
transformMenu,
|
|
transformPage,
|
|
transformProduct,
|
|
transformProducts,
|
|
transformSubCollection
|
|
} from './transform';
|
|
import {
|
|
ApiSchemas,
|
|
CategoryListingResultSW,
|
|
Menu,
|
|
Page,
|
|
Product,
|
|
ProductListingCriteria,
|
|
StoreNavigationTypeSW
|
|
} from './types';
|
|
import { isSeoUrls } from 'lib/shopware/helpers';
|
|
|
|
export async function getMenu(params?: {
|
|
type?: StoreNavigationTypeSW;
|
|
depth?: number;
|
|
}): Promise<Menu[]> {
|
|
const type = params?.type || 'main-navigation';
|
|
const depth = params?.depth || 1;
|
|
const res = await requestNavigation(type, depth);
|
|
|
|
return res ? transformMenu(res, type) : [];
|
|
}
|
|
|
|
export async function getPage(handle: string | []): Promise<Page | undefined> {
|
|
let seoUrlElement;
|
|
let pageIdOrHandle = decodeURIComponent(transformHandle(handle)).replace('cms/', '');
|
|
|
|
if (isSeoUrls()) {
|
|
seoUrlElement = await getFirstSeoUrlElement(pageIdOrHandle);
|
|
if (seoUrlElement) {
|
|
pageIdOrHandle = seoUrlElement.foreignKey;
|
|
}
|
|
|
|
if (!seoUrlElement) {
|
|
console.log('[getPage] Did not found any seoUrl element with page handle:', pageIdOrHandle);
|
|
}
|
|
}
|
|
|
|
const category = await getCategory(pageIdOrHandle);
|
|
if (!category) {
|
|
console.log('[getPage] Did not found any category with handle:', pageIdOrHandle);
|
|
}
|
|
|
|
return category ? transformPage(category, seoUrlElement) : undefined;
|
|
}
|
|
|
|
export async function getFirstSeoUrlElement(
|
|
handle: string
|
|
): Promise<ApiSchemas['SeoUrl'] | undefined> {
|
|
const seoURLCriteria = getSeoUrlCriteria(handle);
|
|
const seoURL = await requestSeoUrl(seoURLCriteria);
|
|
if (seoURL && seoURL.elements && seoURL.elements.length > 0 && seoURL.elements[0]) {
|
|
return seoURL.elements[0];
|
|
}
|
|
}
|
|
|
|
export async function getFirstProduct(productId: string): Promise<ExtendedProduct | undefined> {
|
|
const productCriteria = getDefaultProductCriteria(productId);
|
|
const listing: ExtendedProductListingResult | undefined =
|
|
await requestProductsCollection(productCriteria);
|
|
if (listing && listing.elements && listing.elements.length > 0 && listing.elements[0]) {
|
|
return listing.elements[0];
|
|
}
|
|
}
|
|
|
|
// ToDo: should be more dynamic (depending on handle), should work with server and not client see generateStaticParams from next.js
|
|
export async function getSubCollections(collection: string) {
|
|
const collectionName = decodeURIComponent(transformHandle(collection ?? ''));
|
|
let criteria = getDefaultSubCategoriesCriteria(collectionName);
|
|
let res: CategoryListingResultSW | undefined = undefined;
|
|
const parentCollectionName =
|
|
Array.isArray(collection) && collection[0] ? collection[0] : undefined;
|
|
|
|
if (isSeoUrls()) {
|
|
const seoUrlElement = await getFirstSeoUrlElement(collectionName);
|
|
if (seoUrlElement) {
|
|
criteria = getDefaultSubCategoriesCriteria(seoUrlElement.foreignKey);
|
|
}
|
|
}
|
|
|
|
// @ts-ignore
|
|
res = await requestCategoryList(criteria);
|
|
|
|
return res ? transformSubCollection(res, parentCollectionName) : [];
|
|
}
|
|
|
|
export async function getSearchCollectionProducts(params?: {
|
|
query?: string;
|
|
reverse?: boolean;
|
|
sortKey?: string;
|
|
categoryId?: string;
|
|
defaultSearchCriteria?: Partial<ProductListingCriteria>;
|
|
}) {
|
|
const searchQuery = params?.query ?? '';
|
|
const criteria = getDefaultSearchProductsCriteria(searchQuery);
|
|
const sorting = getSortingCriteria(params?.sortKey, params?.reverse);
|
|
const searchCriteria = { ...criteria, ...sorting };
|
|
|
|
const search = await requestSearchCollectionProducts(searchCriteria);
|
|
if (isSeoUrls() && search) {
|
|
search.elements = await changeVariantUrlToParentUrl(search);
|
|
}
|
|
|
|
return search ? transformProducts(search) : [];
|
|
}
|
|
|
|
export async function changeVariantUrlToParentUrl(
|
|
collection: ExtendedProductListingResult
|
|
): Promise<ExtendedProduct[]> {
|
|
const newElements: ExtendedProduct[] = [];
|
|
if (collection.elements && collection.elements.length > 0) {
|
|
await Promise.all(
|
|
collection.elements.map(async (item) => {
|
|
if (item.parentId && item.seoUrls && item.seoUrls[0]) {
|
|
const parentProduct = await getFirstProduct(item.parentId);
|
|
if (parentProduct && parentProduct.seoUrls && parentProduct.seoUrls[0]) {
|
|
item.seoUrls[0].seoPathInfo = parentProduct.seoUrls[0].seoPathInfo;
|
|
}
|
|
}
|
|
|
|
newElements.push(item);
|
|
})
|
|
);
|
|
}
|
|
|
|
return newElements;
|
|
}
|
|
|
|
export async function getCollectionProducts(params?: {
|
|
collection: string;
|
|
page?: number;
|
|
reverse?: boolean;
|
|
sortKey?: string;
|
|
categoryId?: string;
|
|
defaultSearchCriteria?: Partial<ProductListingCriteria>;
|
|
}): Promise<{ products: Product[]; total: number; limit: number }> {
|
|
let products;
|
|
let category = params?.categoryId;
|
|
const collectionName = decodeURIComponent(transformHandle(params?.collection ?? ''));
|
|
const sorting = getSortingCriteria(params?.sortKey, params?.reverse);
|
|
|
|
if (isSeoUrls() && !category && collectionName !== '') {
|
|
const seoUrlElement = await getFirstSeoUrlElement(collectionName);
|
|
if (seoUrlElement) {
|
|
category = seoUrlElement.foreignKey;
|
|
}
|
|
if (!category) {
|
|
console.log(
|
|
'[useListing][search] Did not found any category with collection name:',
|
|
collectionName
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!isSeoUrls()) {
|
|
category = collectionName ?? undefined;
|
|
}
|
|
|
|
if (category) {
|
|
const criteria = !params?.defaultSearchCriteria
|
|
? getDefaultProductsCriteria(params?.page)
|
|
: params?.defaultSearchCriteria;
|
|
const productsCriteria = { ...criteria, ...sorting };
|
|
products = await requestCategoryProductsCollection(category, productsCriteria);
|
|
if (products) {
|
|
products.elements = await changeVariantUrlToParentUrl(products);
|
|
}
|
|
}
|
|
|
|
return products
|
|
? {
|
|
products: transformProducts(products),
|
|
total: products.total ?? 0,
|
|
limit: products.limit ?? 0
|
|
}
|
|
: { products: [], total: 0, limit: 0 };
|
|
}
|
|
|
|
export async function getCategory(
|
|
categoryId: string,
|
|
cms: boolean = false
|
|
): Promise<ExtendedCategory | undefined> {
|
|
const criteria = cms ? getDefaultCategoryWithCmsCriteria() : getDefaultCategoryCriteria();
|
|
return await requestCategory(categoryId, criteria);
|
|
}
|
|
|
|
// This function is only used for generateMetadata at app/search/(collection)/[...collection]/page.tsx
|
|
export async function getCollection(handle: string | []) {
|
|
let path;
|
|
let seoUrlElement;
|
|
let categoryIdOrHandle = decodeURIComponent(transformHandle(handle));
|
|
|
|
if (isSeoUrls()) {
|
|
seoUrlElement = await getFirstSeoUrlElement(categoryIdOrHandle);
|
|
if (seoUrlElement) {
|
|
categoryIdOrHandle = seoUrlElement.foreignKey;
|
|
path = seoUrlElement.seoPathInfo ?? '';
|
|
}
|
|
}
|
|
|
|
const category = await getCategory(categoryIdOrHandle);
|
|
if (category) {
|
|
const collection = transformCollection(category, seoUrlElement);
|
|
path = path ?? category.id ?? '';
|
|
|
|
return {
|
|
...collection,
|
|
path: `/search/${path}`
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function getProductSeoUrls() {
|
|
const productSeoUrls: { path: string; updatedAt: string }[] = [];
|
|
const seoUrls = await requestSeoUrls('frontend.detail.page');
|
|
|
|
if (seoUrls && seoUrls.elements && seoUrls.elements.length > 0) {
|
|
seoUrls.elements.map((item) =>
|
|
productSeoUrls.push({ path: item.seoPathInfo, updatedAt: item.updatedAt ?? item.createdAt })
|
|
);
|
|
}
|
|
|
|
return productSeoUrls;
|
|
}
|
|
|
|
export async function getProduct(handle: string | []): Promise<Product | undefined> {
|
|
let productSW: ExtendedProduct | undefined;
|
|
let productId: string | undefined;
|
|
const productHandle = decodeURIComponent(transformHandle(handle));
|
|
productId = productHandle; // if we do not use seoUrls the handle should be the product id
|
|
|
|
if (isSeoUrls()) {
|
|
const seoUrlElement = await getFirstSeoUrlElement(productHandle);
|
|
if (seoUrlElement) {
|
|
productId = seoUrlElement.foreignKey;
|
|
}
|
|
}
|
|
|
|
if (!productId) {
|
|
console.log('[getProduct][search] Did not found any product with handle:', handle);
|
|
}
|
|
|
|
if (productId) {
|
|
const firstProduct = await getFirstProduct(productId);
|
|
if (firstProduct) {
|
|
productSW = firstProduct;
|
|
}
|
|
}
|
|
|
|
return productSW ? transformProduct(productSW) : undefined;
|
|
}
|
|
|
|
export async function getProductRecommendations(productId: string): Promise<Product[]> {
|
|
const products: ExtendedProductListingResult = {};
|
|
|
|
const res = await requestCrossSell(productId, getDefaultCrossSellingCriteria());
|
|
// @ToDo: Make this more dynamic to merge multiple Cross-Sellings, at the moment we only get the first one
|
|
if (res && res[0] && res[0].products) {
|
|
products.elements = res[0].products;
|
|
}
|
|
|
|
return products ? transformProducts(products) : [];
|
|
}
|
|
|
|
// This is called from `app/api/revalidate.ts` so providers can control revalidation logic.
|
|
export async function revalidate(req: NextRequest): Promise<NextResponse> {
|
|
return NextResponse.json({
|
|
status: 200,
|
|
message: 'This is currently not working and was never tested.',
|
|
now: Date.now()
|
|
});
|
|
// We always need to respond with a 200 status code,
|
|
// otherwise it will continue to retry the request.
|
|
const collectionWebhooks = ['collections/create', 'collections/delete', 'collections/update'];
|
|
const productWebhooks = ['products/create', 'products/delete', 'products/update'];
|
|
const topic = headers().get('x-shopware-topic') || 'unknown';
|
|
const secret = req.nextUrl.searchParams.get('secret');
|
|
const isCollectionUpdate = collectionWebhooks.includes(topic);
|
|
const isProductUpdate = productWebhooks.includes(topic);
|
|
|
|
if (!secret || secret !== process.env.SHOPWARE_REVALIDATION_SECRET) {
|
|
console.error('Invalid revalidation secret.');
|
|
return NextResponse.json({ status: 200 });
|
|
}
|
|
|
|
if (!isCollectionUpdate && !isProductUpdate) {
|
|
// We don't need to revalidate anything for any other topics.
|
|
return NextResponse.json({ status: 200 });
|
|
}
|
|
|
|
if (isCollectionUpdate) {
|
|
revalidateTag(TAGS.collections);
|
|
}
|
|
|
|
if (isProductUpdate) {
|
|
revalidateTag(TAGS.products);
|
|
}
|
|
|
|
return NextResponse.json({ status: 200, revalidated: true, now: Date.now() });
|
|
}
|