feat(poc): fix type errors in transform, add simple sitemap

This commit is contained in:
Björn Meyer 2023-07-10 16:24:01 +02:00
parent 9c89c36008
commit 1cdb5d3343
6 changed files with 246 additions and 61 deletions

View File

@ -1,4 +1,4 @@
import { getCollections, getPages, getProducts } from 'lib/shopify'; import { getProductSeoUrls } from 'lib/shopware';
import { MetadataRoute } from 'next'; import { MetadataRoute } from 'next';
const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL
@ -11,29 +11,16 @@ export default async function sitemap(): Promise<Promise<Promise<MetadataRoute.S
lastModified: new Date().toISOString() lastModified: new Date().toISOString()
})); }));
const collectionsPromise = getCollections().then((collections) => // @ToDo: Get categories and get cms pages
collections.map((collection) => ({ const productsPromise = getProductSeoUrls().then((products) =>
url: `${baseUrl}${collection.path}`,
lastModified: collection.updatedAt
}))
);
const productsPromise = getProducts({}).then((products) =>
products.map((product) => ({ products.map((product) => ({
url: `${baseUrl}/product/${product.handle}`, url: `${baseUrl}/product/${product.path}`,
lastModified: product.updatedAt lastModified: product.updatedAt
})) }))
); );
const pagesPromise = getPages().then((pages) =>
pages.map((page) => ({
url: `${baseUrl}/${page.handle}`,
lastModified: page.updatedAt
}))
);
const fetchedRoutes = ( const fetchedRoutes = (
await Promise.all([collectionsPromise, productsPromise, pagesPromise]) await Promise.all([productsPromise])
).flat(); ).flat();
return [...routesMap, ...fetchedRoutes]; return [...routesMap, ...fetchedRoutes];

View File

@ -1,17 +1,73 @@
import { operations, operationPaths, components } from '@shopware/api-client/api-types'; import { operations, operationPaths, components } from '@shopware/api-client/api-types';
type schemas = components['schemas'];
type operationsWithoutOriginal = Omit< type operationsWithoutOriginal = Omit<
operations, operations,
'readProduct' | 'searchPage' | 'readProductListing' | 'readCategory'
| 'readCategoryList'
| 'readNavigation'
| 'readProduct'
| 'readProductCrossSellings'
| 'readProductListing'
| 'searchPage'
>; >;
export type extendedPaths = 'readProduct post /product' | operationPaths; export type extendedPaths =
| 'readCategory post /category/{navigationId}?slots'
| 'readCategoryList post /category'
| 'readNavigation post /navigation/{activeId}/{rootId} sw-include-seo-urls'
| 'readProduct post /product'
| 'readProductCrossSellings post /product/{productId}/cross-selling'
| 'readProductListing post /product-listing/{categoryId}'
| 'searchPage post /search'
| operationPaths;
export type extendedOperations = operationsWithoutOriginal & { export type extendedOperations = operationsWithoutOriginal & {
readCategory: extendedReadCategory;
readCategoryList: extendedReadCategoryList;
readNavigation: extendedReadNavigation;
readProduct: extendedReadProduct; readProduct: extendedReadProduct;
searchPage: extendedSearchPage; readProductCrossSellings: extendedReadProductCrossSellings;
readProductListing: extendedReadProductListing; readProductListing: extendedReadProductListing;
searchPage: extendedSearchPage;
}; };
export type ExtendedCriteria = Omit<components['schemas']['Criteria'], 'filter'> & { export type ExtendedCmsBlock = Omit<schemas['CmsBlock'], 'slots'> & {
slots?: schemas['CmsSlot'][];
};
export type ExtendedCmsSection = Omit<schemas['CmsSection'], 'blocks'> & {
blocks?: ExtendedCmsBlock[];
};
export type ExtendedCmsPage = Omit<schemas['CmsPage'], 'sections'> & {
sections?: ExtendedCmsSection[];
};
export type ExtendedProduct = Omit<
schemas['Product'],
'children' | 'seoUrls' | 'options' | 'media'
> & {
children?: ExtendedProduct[];
seoUrls?: schemas['SeoUrl'][];
options?: schemas['PropertyGroupOption'][];
media?: schemas['ProductMedia'][];
};
export type ExtendedProductListingResult = Omit<schemas['ProductListingResult'], 'elements'> & {
elements?: ExtendedProduct[];
};
export type ExtendedCrossSellingElementCollection = Omit<
schemas['CrossSellingElementCollection'],
'products'
> & {
products?: ExtendedProduct[];
};
export type ExtendedCategory = Omit<schemas['Category'], 'children' | 'seoUrls' | 'cmsPage'> & {
children?: ExtendedCategory[];
seoUrls?: schemas['SeoUrl'][];
cmsPage?: ExtendedCmsPage;
};
export type ExtendedCriteria = Omit<schemas['Criteria'], 'filter'> & {
filter?: { filter?: {
field: string; field: string;
type: string; type: string;
@ -30,8 +86,25 @@ type extendedReadProduct = {
200: { 200: {
content: { content: {
'application/json': { 'application/json': {
elements?: components['schemas']['Product'][]; elements?: ExtendedProduct[];
} & components['schemas']['EntitySearchResult']; } & schemas['EntitySearchResult'];
};
};
};
};
type extendedReadProductCrossSellings = {
parameters: {
path: {
/** Product ID */
productId: string;
};
};
responses: {
/** Found cross sellings */
200: {
content: {
'application/json': ExtendedCrossSellingElementCollection
}; };
}; };
}; };
@ -106,14 +179,93 @@ type extendedSearchPage = {
/** Using the search parameter, the server performs a text search on all records based on their data model and weighting as defined in the entity definition using the SearchRanking flag. */ /** Using the search parameter, the server performs a text search on all records based on their data model and weighting as defined in the entity definition using the SearchRanking flag. */
search: string; search: string;
} & ExtendedProductCriteria & } & ExtendedProductCriteria &
components['schemas']['ProductListingFlags']; schemas['ProductListingFlags'];
}; };
}; };
responses: { responses: {
/** Returns a product listing containing all products and additional fields to display a listing. */ /** Returns a product listing containing all products and additional fields to display a listing. */
200: { 200: {
content: { content: {
'application/json': components['schemas']['ProductListingResult']; 'application/json': ExtendedProductListingResult;
};
};
};
};
type extendedReadNavigation = {
parameters: {
header?: {
/** Instructs Shopware to try and resolve SEO URLs for the given navigation item */
'sw-include-seo-urls'?: boolean;
};
path: {
/** Identifier of the active category in the navigation tree (if not used, just set to the same as rootId). */
activeId: string;
/** Identifier of the root category for your desired navigation tree. You can use it to fetch sub-trees of your navigation tree. */
rootId: string;
};
};
requestBody: {
content: {
'application/json': ExtendedCriteria & {
/** Return the categories as a tree or as a flat list. */
buildTree?: unknown;
/** Determines the depth of fetched navigation levels. */
depth?: unknown;
};
};
};
responses: {
/** All available navigations */
200: {
content: {
'application/json': ExtendedCategory[];
};
};
};
};
type extendedReadCategory = {
parameters: {
query?: {
/** Resolves only the given slot identifiers. The identifiers have to be seperated by a '|' character */
slots?: string;
};
path: {
/** Identifier of the category to be fetched */
navigationId: string;
};
};
requestBody?: {
content: {
'application/json': ExtendedCriteria &
Omit<schemas['ProductListingCriteria'], 'filter'> &
ExtendedCriteria;
};
};
responses: {
/** The loaded category with cms page */
200: {
content: {
'application/json': ExtendedCategory;
};
};
};
};
type extendedReadCategoryList = {
requestBody?: {
content: {
'application/json': ExtendedCriteria;
};
};
responses: {
/** Entity search result containing categories. */
200: {
content: {
'application/json': {
elements?: ExtendedCategory[];
} & components['schemas']['EntitySearchResult'];
}; };
}; };
}; };
@ -128,14 +280,14 @@ type extendedReadProductListing = {
}; };
requestBody?: { requestBody?: {
content: { content: {
'application/json': ExtendedProductCriteria & components['schemas']['ProductListingFlags']; 'application/json': ExtendedProductCriteria & schemas['ProductListingFlags'];
}; };
}; };
responses: { responses: {
/** Returns a product listing containing all products and additional fields to display a listing. */ /** Returns a product listing containing all products and additional fields to display a listing. */
200: { 200: {
content: { content: {
'application/json': components['schemas']['ProductListingResult']; 'application/json': ExtendedProductListingResult;
}; };
}; };
}; };

View File

@ -1,10 +1,17 @@
import { createAPIClient, RequestReturnType } from '@shopware/api-client'; import { createAPIClient, RequestReturnType } from '@shopware/api-client';
import { operations } from '@shopware/api-client/api-types'; import { operations } from '@shopware/api-client/api-types';
import { extendedPaths, extendedOperations } from './api-extended';
import { import {
ApiSchemas, ExtendedCategory,
ExtendedCriteria,
ExtendedCrossSellingElementCollection,
ExtendedProductListingResult,
extendedOperations,
extendedPaths
} from './api-extended';
import {
CategoryListingResultSW, CategoryListingResultSW,
ProductListingCriteria, ProductListingCriteria,
RouteNames,
SeoURLResultSW, SeoURLResultSW,
StoreNavigationTypeSW StoreNavigationTypeSW
} from './types'; } from './types';
@ -27,7 +34,7 @@ export type ApiReturnType<OPERATION_NAME extends keyof operations> = RequestRetu
export async function requestNavigation( export async function requestNavigation(
type: StoreNavigationTypeSW, type: StoreNavigationTypeSW,
depth: number depth: number
): Promise<ApiSchemas['NavigationRouteResponse']> { ): Promise<ExtendedCategory[]> {
return await apiInstance.invoke( return await apiInstance.invoke(
'readNavigation post /navigation/{activeId}/{rootId} sw-include-seo-urls', 'readNavigation post /navigation/{activeId}/{rootId} sw-include-seo-urls',
{ {
@ -41,7 +48,7 @@ export async function requestNavigation(
export async function requestCategory( export async function requestCategory(
categoryId: string, categoryId: string,
criteria?: Partial<ProductListingCriteria> criteria?: Partial<ProductListingCriteria>
): Promise<ApiSchemas['Category']> { ): Promise<ExtendedCategory> {
return await apiInstance.invoke('readCategory post /category/{navigationId}?slots', { return await apiInstance.invoke('readCategory post /category/{navigationId}?slots', {
navigationId: categoryId, navigationId: categoryId,
criteria criteria
@ -49,21 +56,21 @@ export async function requestCategory(
} }
export async function requestCategoryList( export async function requestCategoryList(
criteria: Partial<ApiSchemas['Criteria']> criteria: Partial<ExtendedCriteria>
): Promise<CategoryListingResultSW> { ): Promise<CategoryListingResultSW> {
return await apiInstance.invoke('readCategoryList post /category', criteria); return await apiInstance.invoke('readCategoryList post /category', criteria);
} }
export async function requestProductsCollection( export async function requestProductsCollection(
criteria: Partial<ProductListingCriteria> criteria: Partial<ProductListingCriteria>
): Promise<ApiSchemas['ProductListingResult']> { ): Promise<ExtendedProductListingResult> {
return await apiInstance.invoke('readProduct post /product', criteria); return await apiInstance.invoke('readProduct post /product', criteria);
} }
export async function requestCategoryProductsCollection( export async function requestCategoryProductsCollection(
categoryId: string, categoryId: string,
criteria: Partial<ProductListingCriteria> criteria: Partial<ProductListingCriteria>
): Promise<ApiSchemas['ProductListingResult']> { ): Promise<ExtendedProductListingResult> {
return await apiInstance.invoke('readProductListing post /product-listing/{categoryId}', { return await apiInstance.invoke('readProductListing post /product-listing/{categoryId}', {
...criteria, ...criteria,
categoryId: categoryId categoryId: categoryId
@ -72,17 +79,35 @@ export async function requestCategoryProductsCollection(
export async function requestSearchCollectionProducts( export async function requestSearchCollectionProducts(
criteria?: Partial<ProductListingCriteria> criteria?: Partial<ProductListingCriteria>
): Promise<ApiSchemas['ProductListingResult']> { ): Promise<ExtendedProductListingResult> {
return await apiInstance.invoke('searchPage post /search', { return await apiInstance.invoke('searchPage post /search', {
search: encodeURIComponent(criteria?.query || ''), search: encodeURIComponent(criteria?.query || ''),
...criteria ...criteria
}); });
} }
export async function requestSeoUrl(handle: string): Promise<SeoURLResultSW> { export async function requestSeoUrls(routeName: RouteNames, page: number = 1, limit: number = 100) {
return await apiInstance.invoke('readSeoUrl post /seo-url', { return await apiInstance.invoke('readSeoUrl post /seo-url', {
page: 1, page: page,
limit: 1, limit: limit,
filter: [
{
type: 'equals',
field: 'routeName',
value: routeName
}
]
});
}
export async function requestSeoUrl(
handle: string,
page: number = 1,
limit: number = 1
): Promise<SeoURLResultSW> {
return await apiInstance.invoke('readSeoUrl post /seo-url', {
page: page,
limit: limit,
filter: [ filter: [
{ {
type: 'multi', type: 'multi',
@ -108,7 +133,7 @@ export async function requestSeoUrl(handle: string): Promise<SeoURLResultSW> {
export async function requestCrossSell( export async function requestCrossSell(
productId: string, productId: string,
criteria?: Partial<ProductListingCriteria> criteria?: Partial<ProductListingCriteria>
): Promise<ApiSchemas['CrossSellingElementCollection']> { ): Promise<ExtendedCrossSellingElementCollection> {
return await apiInstance.invoke( return await apiInstance.invoke(
'readProductCrossSellings post /product/{productId}/cross-selling', 'readProductCrossSellings post /product/{productId}/cross-selling',
{ {

View File

@ -24,7 +24,8 @@ import {
requestNavigation, requestNavigation,
requestProductsCollection, requestProductsCollection,
requestSearchCollectionProducts, requestSearchCollectionProducts,
requestSeoUrl requestSeoUrl,
requestSeoUrls
} from './api'; } from './api';
import { import {
transformCollection, transformCollection,
@ -35,6 +36,7 @@ import {
transformProducts, transformProducts,
transformStaticCollection transformStaticCollection
} from './transform'; } from './transform';
import { ExtendedCategory, ExtendedProduct, ExtendedProductListingResult } from './api-extended';
export async function getMenu(params?: { export async function getMenu(params?: {
type?: StoreNavigationTypeSW; type?: StoreNavigationTypeSW;
@ -66,11 +68,9 @@ export async function getFirstSeoUrlElement(
} }
} }
export async function getFirstProduct( export async function getFirstProduct(productId: string): Promise<ExtendedProduct | undefined> {
productId: string
): Promise<ApiSchemas['Product'] | undefined> {
const productCriteria = getDefaultProductCriteria(productId); const productCriteria = getDefaultProductCriteria(productId);
const res: ApiSchemas['ProductListingResult'] = await requestProductsCollection(productCriteria); const res: ExtendedProductListingResult = await requestProductsCollection(productCriteria);
if (res.elements && res.elements.length > 0 && res.elements[0]) { if (res.elements && res.elements.length > 0 && res.elements[0]) {
return res.elements[0]; return res.elements[0];
} }
@ -142,7 +142,7 @@ export async function getCollectionProducts(params?: {
export async function getCategory( export async function getCategory(
seoUrl: ApiSchemas['SeoUrl'], seoUrl: ApiSchemas['SeoUrl'],
cms: boolean = false cms: boolean = false
): Promise<ApiSchemas['Category']> { ): Promise<ExtendedCategory> {
const criteria = cms ? getDefaultCategoryWithCmsCriteria() : getDefaultCategoryCriteria(); const criteria = cms ? getDefaultCategoryWithCmsCriteria() : getDefaultCategoryCriteria();
const resCategory = await requestCategory(seoUrl.foreignKey, criteria); const resCategory = await requestCategory(seoUrl.foreignKey, criteria);
@ -165,8 +165,21 @@ export async function getCollection(handle: string | []) {
} }
} }
export async function getProductSeoUrls() {
const productSeoUrls: { path: string; updatedAt: string }[] = [];
const res = await requestSeoUrls('frontend.detail.page');
if (res.elements && res.elements.length > 0) {
res.elements.map((item) =>
productSeoUrls.push({ path: item.seoPathInfo, updatedAt: item.updatedAt ?? item.createdAt })
);
}
return productSeoUrls;
}
export async function getProduct(handle: string | []): Promise<Product | undefined> { export async function getProduct(handle: string | []): Promise<Product | undefined> {
let productSW: ApiSchemas['Product'] | undefined; let productSW: ExtendedProduct | undefined;
let productId: string | undefined; let productId: string | undefined;
const productHandle = transformHandle(handle); const productHandle = transformHandle(handle);
@ -189,11 +202,12 @@ export async function getProduct(handle: string | []): Promise<Product | undefin
} }
export async function getProductRecommendations(productId: string): Promise<Product[]> { export async function getProductRecommendations(productId: string): Promise<Product[]> {
let products: ApiSchemas['ProductListingResult'] = {}; let products: ExtendedProductListingResult = {};
const res = await requestCrossSell(productId, getDefaultCrossSellingCriteria()); 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 // @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) { if (res && res[0] && res[0].products) {
// @ts-ignore (@ToDo: fix this wrong type ...)
products.elements = res[0].products; products.elements = res[0].products;
} }

View File

@ -8,9 +8,15 @@ import {
ProductOption, ProductOption,
ProductVariant ProductVariant
} from './types'; } from './types';
import {
ExtendedCategory,
ExtendedCmsPage,
ExtendedProduct,
ExtendedProductListingResult
} from './api-extended';
import { ListItem } from 'components/layout/search/filter'; import { ListItem } from 'components/layout/search/filter';
export function transformMenu(res: ApiSchemas['NavigationRouteResponse'], type: string) { export function transformMenu(res: ExtendedCategory[], type: string) {
let menu: Menu[] = []; let menu: Menu[] = [];
res.map((item) => menu.push(transformMenuItem(item, type))); res.map((item) => menu.push(transformMenuItem(item, type)));
@ -18,7 +24,7 @@ export function transformMenu(res: ApiSchemas['NavigationRouteResponse'], type:
return menu; return menu;
} }
function transformMenuItem(item: ApiSchemas['Category'], type: string): Menu { function transformMenuItem(item: ExtendedCategory, type: string): Menu {
// @ToDo: currently only footer-navigation is used for cms pages, this need to be more dynamic (shoud depending on the item) // @ToDo: currently only footer-navigation is used for cms pages, this need to be more dynamic (shoud depending on the item)
return { return {
id: item.id ?? '', id: item.id ?? '',
@ -36,11 +42,11 @@ function transformMenuItem(item: ApiSchemas['Category'], type: string): Menu {
export function transformPage( export function transformPage(
seoUrlElement: ApiSchemas['SeoUrl'], seoUrlElement: ApiSchemas['SeoUrl'],
category: ApiSchemas['Category'] category: ExtendedCategory
): Page { ): Page {
let plainHtmlContent; let plainHtmlContent;
if (category.cmsPage) { if (category.cmsPage) {
const cmsPage: ApiSchemas['CmsPage'] = category.cmsPage; const cmsPage: ExtendedCmsPage = category.cmsPage;
plainHtmlContent = transformToPlainHtmlContent(cmsPage); plainHtmlContent = transformToPlainHtmlContent(cmsPage);
} }
@ -62,7 +68,7 @@ export function transformPage(
}; };
} }
export function transformToPlainHtmlContent(cmsPage: ApiSchemas['CmsPage']): string { export function transformToPlainHtmlContent(cmsPage: ExtendedCmsPage): string {
let plainHtmlContent = ''; let plainHtmlContent = '';
cmsPage.sections?.map((section) => { cmsPage.sections?.map((section) => {
@ -84,7 +90,7 @@ export function transformToPlainHtmlContent(cmsPage: ApiSchemas['CmsPage']): str
export function transformCollection( export function transformCollection(
seoUrlElement: ApiSchemas['SeoUrl'], seoUrlElement: ApiSchemas['SeoUrl'],
resCategory: ApiSchemas['Category'] resCategory: ExtendedCategory
) { ) {
return { return {
handle: seoUrlElement.seoPathInfo, handle: seoUrlElement.seoPathInfo,
@ -137,7 +143,7 @@ export function transformStaticCollectionToList(collection: Collection[]): ListI
return listItem; return listItem;
} }
export function transformProducts(res: ApiSchemas['ProductListingResult']): Product[] { export function transformProducts(res: ExtendedProductListingResult): Product[] {
let products: Product[] = []; let products: Product[] = [];
if (res.elements && res.elements.length > 0) { if (res.elements && res.elements.length > 0) {
@ -147,7 +153,7 @@ export function transformProducts(res: ApiSchemas['ProductListingResult']): Prod
return products; return products;
} }
export function transformProduct(item: ApiSchemas['Product']): Product { export function transformProduct(item: ExtendedProduct): Product {
const productOptions = transformOptions(item); const productOptions = transformOptions(item);
const productVariants = transformVariants(item); const productVariants = transformVariants(item);
@ -202,7 +208,7 @@ export function transformProduct(item: ApiSchemas['Product']): Product {
}; };
} }
function transformOptions(parent: ApiSchemas['Product']): ProductOption[] { function transformOptions(parent: ExtendedProduct): ProductOption[] {
// we only transform options for parents with children, ignore child products with options // we only transform options for parents with children, ignore child products with options
let productOptions: ProductOption[] = []; let productOptions: ProductOption[] = [];
if (parent.children && parent.parentId === null && parent.children.length > 0) { if (parent.children && parent.parentId === null && parent.children.length > 0) {
@ -231,7 +237,7 @@ function transformOptions(parent: ApiSchemas['Product']): ProductOption[] {
return productOptions; return productOptions;
} }
function transformVariants(parent: ApiSchemas['Product']): ProductVariant[] { function transformVariants(parent: ExtendedProduct): ProductVariant[] {
let productVariants: ProductVariant[] = []; let productVariants: ProductVariant[] = [];
if (parent.children && parent.parentId === null && parent.children.length > 0) { if (parent.children && parent.parentId === null && parent.children.length > 0) {
parent.children.map((child) => { parent.children.map((child) => {

View File

@ -1,5 +1,5 @@
import { components } from '@shopware/api-client/api-types'; import { components } from '@shopware/api-client/api-types';
import { ExtendedCriteria } from './api-extended'; import { ExtendedCriteria, ExtendedCategory, ExtendedCmsPage } from './api-extended';
/** Shopware Types */ /** Shopware Types */
@ -10,10 +10,11 @@ export type ProductListingCriteria = {
query: string; query: string;
} & Omit<ApiSchemas['ProductListingCriteria'], 'filter'> & } & Omit<ApiSchemas['ProductListingCriteria'], 'filter'> &
ExtendedCriteria; ExtendedCriteria;
export type RouteNames = 'frontend.navigation.page' | 'frontend.detail.page' | 'frontend.account.customer-group-registration.page' | 'frontend.landing.page'
/** Return Types */ /** Return Types */
export type CategoryListingResultSW = { export type CategoryListingResultSW = {
elements?: ApiSchemas['Category'][]; elements?: ExtendedCategory[];
} & ApiSchemas['EntitySearchResult']; } & ApiSchemas['EntitySearchResult'];
export type SeoURLResultSW = { export type SeoURLResultSW = {
elements?: ApiSchemas['SeoUrl'][]; elements?: ApiSchemas['SeoUrl'][];
@ -33,7 +34,7 @@ export type Page = {
updatedAt: string; updatedAt: string;
routeName?: string; routeName?: string;
foreignKey?: string; foreignKey?: string;
originalCmsPage?: ApiSchemas['CmsPage']; originalCmsPage?: ExtendedCmsPage;
}; };
export type ProductOption = { export type ProductOption = {