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';
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()
}));
const collectionsPromise = getCollections().then((collections) =>
collections.map((collection) => ({
url: `${baseUrl}${collection.path}`,
lastModified: collection.updatedAt
}))
);
const productsPromise = getProducts({}).then((products) =>
// @ToDo: Get categories and get cms pages
const productsPromise = getProductSeoUrls().then((products) =>
products.map((product) => ({
url: `${baseUrl}/product/${product.handle}`,
url: `${baseUrl}/product/${product.path}`,
lastModified: product.updatedAt
}))
);
const pagesPromise = getPages().then((pages) =>
pages.map((page) => ({
url: `${baseUrl}/${page.handle}`,
lastModified: page.updatedAt
}))
);
const fetchedRoutes = (
await Promise.all([collectionsPromise, productsPromise, pagesPromise])
await Promise.all([productsPromise])
).flat();
return [...routesMap, ...fetchedRoutes];

View File

@ -1,17 +1,73 @@
import { operations, operationPaths, components } from '@shopware/api-client/api-types';
type schemas = components['schemas'];
type operationsWithoutOriginal = Omit<
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 & {
readCategory: extendedReadCategory;
readCategoryList: extendedReadCategoryList;
readNavigation: extendedReadNavigation;
readProduct: extendedReadProduct;
searchPage: extendedSearchPage;
readProductCrossSellings: extendedReadProductCrossSellings;
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?: {
field: string;
type: string;
@ -30,8 +86,25 @@ type extendedReadProduct = {
200: {
content: {
'application/json': {
elements?: components['schemas']['Product'][];
} & components['schemas']['EntitySearchResult'];
elements?: ExtendedProduct[];
} & 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. */
search: string;
} & ExtendedProductCriteria &
components['schemas']['ProductListingFlags'];
schemas['ProductListingFlags'];
};
};
responses: {
/** Returns a product listing containing all products and additional fields to display a listing. */
200: {
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?: {
content: {
'application/json': ExtendedProductCriteria & components['schemas']['ProductListingFlags'];
'application/json': ExtendedProductCriteria & schemas['ProductListingFlags'];
};
};
responses: {
/** Returns a product listing containing all products and additional fields to display a listing. */
200: {
content: {
'application/json': components['schemas']['ProductListingResult'];
'application/json': ExtendedProductListingResult;
};
};
};

View File

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

View File

@ -24,7 +24,8 @@ import {
requestNavigation,
requestProductsCollection,
requestSearchCollectionProducts,
requestSeoUrl
requestSeoUrl,
requestSeoUrls
} from './api';
import {
transformCollection,
@ -35,6 +36,7 @@ import {
transformProducts,
transformStaticCollection
} from './transform';
import { ExtendedCategory, ExtendedProduct, ExtendedProductListingResult } from './api-extended';
export async function getMenu(params?: {
type?: StoreNavigationTypeSW;
@ -66,11 +68,9 @@ export async function getFirstSeoUrlElement(
}
}
export async function getFirstProduct(
productId: string
): Promise<ApiSchemas['Product'] | undefined> {
export async function getFirstProduct(productId: string): Promise<ExtendedProduct | undefined> {
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]) {
return res.elements[0];
}
@ -142,7 +142,7 @@ export async function getCollectionProducts(params?: {
export async function getCategory(
seoUrl: ApiSchemas['SeoUrl'],
cms: boolean = false
): Promise<ApiSchemas['Category']> {
): Promise<ExtendedCategory> {
const criteria = cms ? getDefaultCategoryWithCmsCriteria() : getDefaultCategoryCriteria();
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> {
let productSW: ApiSchemas['Product'] | undefined;
let productSW: ExtendedProduct | undefined;
let productId: string | undefined;
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[]> {
let products: ApiSchemas['ProductListingResult'] = {};
let 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) {
// @ts-ignore (@ToDo: fix this wrong type ...)
products.elements = res[0].products;
}

View File

@ -8,9 +8,15 @@ import {
ProductOption,
ProductVariant
} from './types';
import {
ExtendedCategory,
ExtendedCmsPage,
ExtendedProduct,
ExtendedProductListingResult
} from './api-extended';
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[] = [];
res.map((item) => menu.push(transformMenuItem(item, type)));
@ -18,7 +24,7 @@ export function transformMenu(res: ApiSchemas['NavigationRouteResponse'], type:
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)
return {
id: item.id ?? '',
@ -36,11 +42,11 @@ function transformMenuItem(item: ApiSchemas['Category'], type: string): Menu {
export function transformPage(
seoUrlElement: ApiSchemas['SeoUrl'],
category: ApiSchemas['Category']
category: ExtendedCategory
): Page {
let plainHtmlContent;
if (category.cmsPage) {
const cmsPage: ApiSchemas['CmsPage'] = category.cmsPage;
const cmsPage: ExtendedCmsPage = category.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 = '';
cmsPage.sections?.map((section) => {
@ -84,7 +90,7 @@ export function transformToPlainHtmlContent(cmsPage: ApiSchemas['CmsPage']): str
export function transformCollection(
seoUrlElement: ApiSchemas['SeoUrl'],
resCategory: ApiSchemas['Category']
resCategory: ExtendedCategory
) {
return {
handle: seoUrlElement.seoPathInfo,
@ -137,7 +143,7 @@ export function transformStaticCollectionToList(collection: Collection[]): ListI
return listItem;
}
export function transformProducts(res: ApiSchemas['ProductListingResult']): Product[] {
export function transformProducts(res: ExtendedProductListingResult): Product[] {
let products: Product[] = [];
if (res.elements && res.elements.length > 0) {
@ -147,7 +153,7 @@ export function transformProducts(res: ApiSchemas['ProductListingResult']): Prod
return products;
}
export function transformProduct(item: ApiSchemas['Product']): Product {
export function transformProduct(item: ExtendedProduct): Product {
const productOptions = transformOptions(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
let productOptions: ProductOption[] = [];
if (parent.children && parent.parentId === null && parent.children.length > 0) {
@ -231,7 +237,7 @@ function transformOptions(parent: ApiSchemas['Product']): ProductOption[] {
return productOptions;
}
function transformVariants(parent: ApiSchemas['Product']): ProductVariant[] {
function transformVariants(parent: ExtendedProduct): ProductVariant[] {
let productVariants: ProductVariant[] = [];
if (parent.children && parent.parentId === null && parent.children.length > 0) {
parent.children.map((child) => {

View File

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