Skip to content
5 changes: 5 additions & 0 deletions .changeset/bright-foxes-arrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nextjs-website": patch
---

Refactor api layer for api data model
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getApiData } from '@/lib/api';
import { ApiDataListRepository } from '@/lib/apiDataList';
import ProductLayout, {
ProductLayoutProps,
} from '@/components/organisms/ProductLayout/ProductLayout';
Expand Down Expand Up @@ -41,7 +41,14 @@ export const generateMetadata = async (
): Promise<Metadata> => {
const params = await props.params;
const resolvedParent = await parent;
const ApiDataProps = await getApiData(params.locale, params.apiDataSlug);
const ApiDataProps = await ApiDataListRepository.getBySlug(
params.locale,
params.apiDataSlug
);
if (!ApiDataProps) {
// eslint-disable-next-line functional/no-throw-statements
throw new Error('Failed to fetch data');
}

if (ApiDataProps?.seo) {
return makeMetadataFromStrapi(ApiDataProps.seo);
Expand All @@ -59,7 +66,14 @@ export const generateMetadata = async (

const ApiDataPage = async (props: ApiDataParams) => {
const params = await props.params;
const apiDataProps = await getApiData(params.locale, params.apiDataSlug);
const apiDataProps = await ApiDataListRepository.getBySlug(
params.locale,
params.apiDataSlug
);
if (!apiDataProps) {
// eslint-disable-next-line functional/no-throw-statements
throw new Error('Failed to fetch data');
}

const structuredData = generateStructuredDataScripts({
breadcrumbsItems: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ import {
makeMetadata,
makeMetadataFromStrapi,
} from '@/helpers/metadata.helpers';
import { getApiDataListPages } from '@/lib/api';
import { ApiDataListPagesRepository } from '@/lib/apiDataListPages';
import { Metadata } from 'next';
import { SUPPORTED_LOCALES } from '@/locales';

type Params = {
locale: string;
Expand All @@ -23,7 +22,10 @@ export async function generateMetadata(props: {
params: Promise<Params>;
}): Promise<Metadata> {
const { locale, productSlug } = await props.params;
const apiDataListPage = await getApiDataListPages(locale, productSlug);
const apiDataListPage = await ApiDataListPagesRepository.getByProductSlug(
locale,
productSlug
);

if (apiDataListPage?.seo) {
return makeMetadataFromStrapi(apiDataListPage.seo);
Expand All @@ -40,7 +42,8 @@ export async function generateMetadata(props: {

const ApiDataListPage = async (props: { params: Promise<Params> }) => {
const { locale, productSlug } = await props.params;
const apiDataListPageProps = await getApiDataListPages(locale, productSlug);
const apiDataListPageProps =
await ApiDataListPagesRepository.getByProductSlug(locale, productSlug);

const structuredData = generateStructuredDataScripts({
breadcrumbsItems: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import BannerLinks from '@/components/molecules/BannerLinks/BannerLinks';
import { useTranslations } from 'next-intl';
import { SEO } from '@/lib/types/seo';
import { Product } from '@/lib/types/product';
import { BaseApiDataList } from '@/lib/apiDataList/types';
import { Tag } from '@/lib/types/tag';
import { CardProps } from '@/components/molecules/CardsGrid/CardsGrid';
import { StrapiApiDataList } from '@/lib/strapi/types/apiDataList';

export type ApiDataListPageTemplateProps = {
readonly hero: {
Expand All @@ -29,7 +29,7 @@ export type ApiDataListPageTemplateProps = {
readonly updatedAt: string;
readonly bannerLinks: BannerLinkProps[];
readonly theme?: Theme;
readonly api_data: StrapiApiDataList;
readonly apiData: BaseApiDataList;
readonly seo?: SEO;
};

Expand Down
21 changes: 2 additions & 19 deletions apps/nextjs-website/src/lib/api.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { ApiDataListPagesRepository } from '@/lib/apiDataListPages';
import { Product } from './types/product';
import { Webinar } from '@/lib/types/webinar';
import {
getApiDataListPagesProps,
getApiDataProps,
getCaseHistoriesProps,
getGuideListPagesProps,
getGuidePageProps,
Expand Down Expand Up @@ -181,7 +180,7 @@ export async function getCaseHistory(locale: string, caseHistorySlug?: string) {
}

export async function getApiDataParams(locale: string) {
const props = (await getApiDataListPagesProps(locale)).flatMap(
const props = (await ApiDataListPagesRepository.getAll(locale)).flatMap(
(apiDataListPageProps) =>
apiDataListPageProps.apiDetailSlugs.map((apiDataSlug) => ({
productSlug: apiDataListPageProps.product.slug,
Expand All @@ -193,29 +192,13 @@ export async function getApiDataParams(locale: string) {
return props || [];
}

export async function getApiDataListPages(locale: string, productSlug: string) {
const props = (await getApiDataListPagesProps(locale)).find(
(apiDataListPageProps) => apiDataListPageProps.product.slug === productSlug
);
return props;
}

export async function getProduct(locale: string, productSlug: string) {
const props = (await getProductsProps(locale)).find(
(product) => product.slug === productSlug
);
return props;
}

export async function getApiData(locale: string, apiDataSlug: string) {
const props = manageUndefined(
(await getApiDataProps(locale)).find(
(apiData) => apiData.apiDataSlug === apiDataSlug
)
);
return props;
}

export async function getReleaseNote(
locale: string,
productSlug: string,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { makeApiDataListProps } from '@/lib/strapi/makeProps/makeApiDataList';
import { StrapiApiDataList } from '@/lib/strapi/types/apiDataList';
import { mapApiDataList } from '@/lib/apiDataList/mapper';
import { ApiDataList } from '@/lib/apiDataList/types';
import _ from 'lodash';
import {
strapiApiDataList,
Expand Down Expand Up @@ -30,7 +30,7 @@ jest.mock('@/lib/strapi/makeProps/makeApiSoapUrlList', () => ({
]),
}));

describe('makeApiDataListProps', () => {
describe('mapApiDataList', () => {
beforeEach(() => {
spyOnConsoleError.mockClear();
});
Expand All @@ -40,20 +40,14 @@ describe('makeApiDataListProps', () => {
});

it('should transform strapi api data list to api data page props', async () => {
const result = await makeApiDataListProps(
'it',
_.cloneDeep({ data: strapiApiDataList })
);
const result = await mapApiDataList('it', _.cloneDeep(strapiApiDataList));
expect(result).toHaveLength(2);
expect(result[0]).toMatchObject(expectedApiDataPageProps[0]);
expect(result[1]).toMatchObject(expectedApiDataPageProps[1]);
});

it('should handle minimal data with missing optional fields', async () => {
const result = await makeApiDataListProps(
'it',
_.cloneDeep({ data: minimalApiDataList() })
);
const result = await mapApiDataList('it', minimalApiDataList());
expect(result).toHaveLength(1);
const firstElement = result[0];
expect(firstElement.title).toBe('Minimal API Data');
Expand All @@ -64,32 +58,31 @@ describe('makeApiDataListProps', () => {
});

it('should handle empty data array', async () => {
const emptyData: StrapiApiDataList = [];
const result = await makeApiDataListProps('it', { data: [...emptyData] });
const emptyData: ApiDataList = {
data: [],
};
const result = await mapApiDataList('it', emptyData);
expect(result).toHaveLength(0);
});

it('should use product banner links when api data banner links are empty', async () => {
const result = await makeApiDataListProps('it', {
data: apiDataWithoutBannerLinks(),
});
const result = await mapApiDataList('it', apiDataWithoutBannerLinks());
const firstElement = result[0];
expect(firstElement.bannerLinks).toBeDefined();
expect(firstElement.bannerLinks).toHaveLength(1);
expect(firstElement.bannerLinks?.[0].title).toBe('Banner Link 1');
});

it('should filter out api data without rest or soap details', async () => {
const result = await makeApiDataListProps('it', {
data: apiDataWithoutApiDetails(),
});
const result = await mapApiDataList('it', apiDataWithoutApiDetails());
expect(result).toHaveLength(0);
});

it('should filter out api data with rest api details with invalid data', async () => {
const result = await makeApiDataListProps('it', {
data: apiDataWithInvalidRestApiDetails(),
});
const result = await mapApiDataList(
'it',
apiDataWithInvalidRestApiDetails()
);
expect(result).toHaveLength(0);
expect(spyOnConsoleError).toHaveBeenCalledWith(
expect.stringContaining(
Expand All @@ -99,9 +92,7 @@ describe('makeApiDataListProps', () => {
});

it('should filter out api data with soap api details without slug', async () => {
const result = await makeApiDataListProps('it', {
data: apiDatalistWithItemMissingSlug(),
});
const result = await mapApiDataList('it', apiDatalistWithItemMissingSlug());
expect(result).toHaveLength(0);
expect(spyOnConsoleError).toHaveBeenCalledWith(
expect.stringContaining(
Expand All @@ -111,9 +102,7 @@ describe('makeApiDataListProps', () => {
});

it('should handle mixed valid and invalid api data', async () => {
const result = await makeApiDataListProps('it', {
data: mixedApiDataValidAndInvalid(),
});
const result = await mapApiDataList('it', mixedApiDataValidAndInvalid());

// Should return only the 3 valid api data items, filtering out invalid ones
expect(result).toHaveLength(3);
Expand All @@ -124,24 +113,21 @@ describe('makeApiDataListProps', () => {
});

it('should handle api data without banner links and without product banner links', async () => {
const result = await makeApiDataListProps('it', {
data: apiDataWithoutProductBannerLinks(),
});
const result = await mapApiDataList(
'it',
apiDataWithoutProductBannerLinks()
);
expect(result[0].bannerLinks).toEqual([]);
});

it('should return empty array when all api data are invalid', async () => {
const result = await makeApiDataListProps('it', {
data: [...allInvalidApiData()],
});
const result = await mapApiDataList('it', allInvalidApiData());
expect(result).toHaveLength(0);
expect(spyOnConsoleError).toHaveBeenCalled();
});

it('should correctly identify REST API type', async () => {
const result = await makeApiDataListProps('it', {
data: restApiDataOnly(),
});
const result = await mapApiDataList('it', restApiDataOnly());
const firstElement = result[0];
expect(firstElement.apiType).toBe('rest');
expect(firstElement.restApiSpecUrls).toHaveLength(1);
Expand All @@ -150,9 +136,7 @@ describe('makeApiDataListProps', () => {
});

it('should correctly identify SOAP API type', async () => {
const result = await makeApiDataListProps('it', {
data: soapApiDataOnly(),
});
const result = await mapApiDataList('it', soapApiDataOnly());
const firstElement = result[0];
expect(firstElement.apiType).toBe('soap');
expect(firstElement.restApiSpecUrls).toEqual([]);
Expand All @@ -163,53 +147,41 @@ describe('makeApiDataListProps', () => {
});

it('should prioritize api data banner links over product banner links', async () => {
const result = await makeApiDataListProps('it', {
data: strapiApiDataList,
});
const result = await mapApiDataList('it', strapiApiDataList);
const firstElement = result[0];
expect(firstElement.bannerLinks).toHaveLength(2);
expect(firstElement.bannerLinks?.[0].title).toBe('Banner Link 1');
expect(firstElement.bannerLinks?.[1].title).toBe('Banner Link 2');
});

it('should handle api data with product that has undefined banner links', async () => {
const result = await makeApiDataListProps('it', {
data: minimalApiDataList(),
});
const result = await mapApiDataList('it', minimalApiDataList());
expect(result[0].bannerLinks).toBeUndefined();
});

it('should set correct specUrlsName from title', async () => {
const result = await makeApiDataListProps('it', {
data: strapiApiDataList,
});
const result = await mapApiDataList('it', strapiApiDataList);
expect(result[0].specUrlsName).toBe('SEND Main');
expect(result[1].specUrlsName).toBe('Documentazione SOAP');
});

it('should handle REST API with multiple spec URLs', async () => {
const result = await makeApiDataListProps('it', {
data: restApiDataWithMultipleSpecs(),
});
const result = await mapApiDataList('it', restApiDataWithMultipleSpecs());
const firstElement = result[0];
expect(firstElement.restApiSpecUrls).toHaveLength(2);
expect(firstElement.restApiSpecUrls[0].name).toBe('API 1');
expect(firstElement.restApiSpecUrls[1].hideTryIt).toBe(true);
});

it('should handle SOAP API and call makeApiSoapUrlList', async () => {
const result = await makeApiDataListProps('it', {
data: soapApiDataOnly(),
});
const result = await mapApiDataList('it', soapApiDataOnly());
expect(result[0].apiSoapUrlList).toEqual([
{ name: 'test.wsdl', url: 'https://example.com/test.wsdl' },
]);
});

it('should handle api data with missing product gracefully', async () => {
const result = await makeApiDataListProps('it', {
data: apiDataWithMissingProduct(),
});
const result = await mapApiDataList('it', apiDataWithMissingProduct());
// Should filter out items with missing product since makeBaseProductWithoutLogoProps would fail
expect(result).toHaveLength(0);
expect(spyOnConsoleError).toHaveBeenCalledWith(
Expand Down
34 changes: 34 additions & 0 deletions apps/nextjs-website/src/lib/apiDataList/fetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import qs from 'qs';
import { fetchFromStrapi } from '@/lib/strapi/fetchFromStrapi';
import { productRelationsPopulate } from '@/lib/strapi/fetches/fetchProducts';
import { ApiDataList } from './types';

import { buildEnv } from '@/lib/buildEnv';

const makeStrapiApiDataListPopulate = () =>
qs.stringify({
populate: {
apiRestDetail: {
populate: ['slug', 'specUrls'],
},
apiSoapDetail: {
populate: ['slug', 'repositoryUrl', 'dirName'],
},
icon: { populate: '*' },
product: {
...productRelationsPopulate,
},
bannerLinks: {
populate: ['icon'],
},
seo: {
populate: '*,metaImage,metaSocial.image',
},
},
});

export const fetchApiDataList = (locale: string) =>
fetchFromStrapi<ApiDataList>('apis-data', makeStrapiApiDataListPopulate())(
locale,
buildEnv
);
Loading
Loading