diff --git a/api/shopware-6-client.api.md b/api/shopware-6-client.api.md index ae3ded8ad..fbb857943 100644 --- a/api/shopware-6-client.api.md +++ b/api/shopware-6-client.api.md @@ -18,6 +18,7 @@ import { EqualsAnyFilter } from '@shopware-pwa/commons/interfaces/search/SearchF import { EqualsFilter } from '@shopware-pwa/commons/interfaces/search/SearchFilter'; import { Grouping } from '@shopware-pwa/commons/interfaces/search/Grouping'; import { GuestOrderParams } from '@shopware-pwa/commons/interfaces/request/GuestOrderParams'; +import { Includes } from '@shopware-pwa/commons/interfaces/search/SearchCriteria'; import { Language } from '@shopware-pwa/commons/interfaces/models/framework/language/Language'; import { MultiFilter } from '@shopware-pwa/commons/interfaces/search/SearchFilter'; import { NavigationResponse } from '@shopware-pwa/commons/interfaces/models/content/navigation/Navigation'; @@ -342,6 +343,8 @@ export interface ShopwareParams { // (undocumented) grouping?: Grouping; // (undocumented) + includes?: Includes; + // (undocumented) limit?: number; // (undocumented) manufacturer?: string; diff --git a/docs/landing/concepts/cms.md b/docs/landing/concepts/cms.md index 9e0e24814..dfd402e5f 100644 --- a/docs/landing/concepts/cms.md +++ b/docs/landing/concepts/cms.md @@ -444,4 +444,12 @@ Finally we have our desired effect: ![preview](./../../assets/preview2.png) -And that's it. This set of rules applies for sections, blocks and elements. \ No newline at end of file +And that's it. This set of rules applies for sections, blocks and elements. + + +## Performance +The Shopware6 API returns all data that you may need, and even more. This is why an `includes` parameter was invented - [Read more at _Includes_ chapter here](https://docs.shopware.com/en/shopware-platform-dev-en/admin-api-guide/reading-entities). + +In order to make the response payload smaller, aligned with our needs - the `includes` parameter is being used. + +The list of all fetched fields within the page resolver, which is responsible for getting the CMS content (but not only) can be found [here (_getPageIncludes_ method)](../../../packages/composables/src/internalHelpers/includesParameter.ts). Thanks to this mechanism - the shopware-pwa fetches only the relevant data. \ No newline at end of file diff --git a/packages/commons/interfaces/search/SearchCriteria.ts b/packages/commons/interfaces/search/SearchCriteria.ts index 7fa11f1a5..60313e2f6 100644 --- a/packages/commons/interfaces/search/SearchCriteria.ts +++ b/packages/commons/interfaces/search/SearchCriteria.ts @@ -26,6 +26,13 @@ export interface Grouping { field: string; } +/** + * @beta + */ +export interface Includes { + [key: string]: string[]; +} + /** * configutarion.displayParents: true - if you want to show all the products * @alpha @@ -43,6 +50,7 @@ export interface SearchCriteria { associations?: Association[]; aggregations?: Aggregation[]; totalCountMode?: TotalCountMode; + includes?: Includes; }; } @@ -70,4 +78,5 @@ export interface ShopwareSearchParams { grouping?: Grouping; properties?: string | undefined | never[]; manufacturer?: string | undefined | never[]; + includes?: Includes; } diff --git a/packages/composables/__tests__/useCms.spec.ts b/packages/composables/__tests__/useCms.spec.ts index 1d099ff89..6d628c9dc 100644 --- a/packages/composables/__tests__/useCms.spec.ts +++ b/packages/composables/__tests__/useCms.spec.ts @@ -9,6 +9,7 @@ Vue.use(VueCompositionApi); import { useCms } from "@shopware-pwa/composables"; import * as shopwareClient from "@shopware-pwa/shopware-6-client"; +import { getPageIncludes } from "../src/internalHelpers/includesParameter"; jest.mock("@shopware-pwa/shopware-6-client"); const mockedGetPage = shopwareClient as jest.Mocked; @@ -69,42 +70,112 @@ describe("Composables - useCms", () => { expect(error.value).toStrictEqual({ message: "Something went wrong..." }); }); - it("should performs search default pagination limit if not provided", async () => { - const { search, page } = useCms(rootContextMock); - mockedGetPage.getPage.mockResolvedValueOnce({} as any); - expect(page.value).toEqual(null); - await search(""); - expect(mockedGetPage.getPage).toBeCalledWith( - "", - { - configuration: { - associations: [ - { associations: [{ name: "group" }], name: "options" }, - ], - }, - pagination: { limit: 10 }, - }, - rootContextMock.$shopwareApiInstance - ); - }); + describe("methods", () => { + describe("search", () => { + it("should performs search default pagination limit if not provided", async () => { + const { search, page } = useCms(rootContextMock); + mockedGetPage.getPage.mockResolvedValueOnce({} as any); + expect(page.value).toEqual(null); + await search(""); + expect(mockedGetPage.getPage).toBeCalledWith( + "", + { + configuration: { + associations: [ + { associations: [{ name: "group" }], name: "options" }, + ], + includes: getPageIncludes(), + }, + pagination: { limit: 10 }, + }, + rootContextMock.$shopwareApiInstance + ); + }); - it("should perform search default pagination limit if wrong value", async () => { - const { search, page } = useCms(rootContextMock); - mockedGetPage.getPage.mockResolvedValueOnce({} as any); - expect(page.value).toEqual(null); - await search("", { pagination: "null" }); - expect(mockedGetPage.getPage).toBeCalledWith( - "", - { - configuration: { - associations: [ - { associations: [{ name: "group" }], name: "options" }, - ], - }, - pagination: { limit: 10 }, - }, - rootContextMock.$shopwareApiInstance - ); + it("should perform search default pagination limit if wrong value", async () => { + const { search, page } = useCms(rootContextMock); + mockedGetPage.getPage.mockResolvedValueOnce({} as any); + expect(page.value).toEqual(null); + await search("", { pagination: "null" }); + expect(mockedGetPage.getPage).toBeCalledWith( + "", + { + configuration: { + associations: [ + { associations: [{ name: "group" }], name: "options" }, + ], + includes: getPageIncludes(), + }, + pagination: { limit: 10 }, + }, + rootContextMock.$shopwareApiInstance + ); + }); + + it("should performs search with pagination if provided", async () => { + const { search, page } = useCms(rootContextMock); + mockedGetPage.getPage.mockResolvedValueOnce({} as any); + expect(page.value).toEqual(null); + await search("", { pagination: { limit: 50 } }); + expect(mockedGetPage.getPage).toBeCalledWith( + "", + { + configuration: { + associations: [ + { associations: [{ name: "group" }], name: "options" }, + ], + includes: getPageIncludes(), + }, + pagination: { limit: 50 }, + }, + rootContextMock.$shopwareApiInstance + ); + }); + + it("should provide default includes if not provided, but configuration exist", async () => { + const { search, page } = useCms(rootContextMock); + mockedGetPage.getPage.mockResolvedValueOnce({} as any); + expect(page.value).toEqual(null); + await search("", { + configuration: {}, + }); + expect(mockedGetPage.getPage).toBeCalledWith( + "", + { + configuration: { + associations: [ + { associations: [{ name: "group" }], name: "options" }, + ], + includes: getPageIncludes(), + }, + pagination: { limit: 10 }, + }, + rootContextMock.$shopwareApiInstance + ); + }); + + it("should invoke search with custom includes", async () => { + const { search, page } = useCms(rootContextMock); + mockedGetPage.getPage.mockResolvedValueOnce({} as any); + expect(page.value).toEqual(null); + await search("", { + configuration: { includes: { product: ["name"] } }, + }); + expect(mockedGetPage.getPage).toBeCalledWith( + "", + { + configuration: { + associations: [ + { associations: [{ name: "group" }], name: "options" }, + ], + includes: { product: ["name"] }, + }, + pagination: { limit: 10 }, + }, + rootContextMock.$shopwareApiInstance + ); + }); + }); }); it("should return activeCategoryId if it's included within the page object", async () => { diff --git a/packages/composables/src/hooks/useCms/index.ts b/packages/composables/src/hooks/useCms/index.ts index b0e9cd515..86a1c94fe 100644 --- a/packages/composables/src/hooks/useCms/index.ts +++ b/packages/composables/src/hooks/useCms/index.ts @@ -5,6 +5,7 @@ import { parseUrlQuery } from "@shopware-pwa/helpers"; import { ClientApiError } from "@shopware-pwa/commons/interfaces/errors/ApiError"; import { getApplicationContext } from "@shopware-pwa/composables"; import { ApplicationVueContext } from "../../appContext"; +import { getPageIncludes } from "../../internalHelpers/includesParameter"; /** * @alpha @@ -36,12 +37,10 @@ export const useCms = (rootContext: ApplicationVueContext): any => { if (!searchCriteria.configuration) searchCriteria.configuration = {}; // Temp solution for consistant page size // @TODO: https://github.com/DivanteLtd/shopware-pwa/issues/739 - /* istanbul ignore else */ if (!searchCriteria.pagination || searchCriteria.pagination === "null") { searchCriteria.pagination = {}; } - /* istanbul ignore else */ if (!searchCriteria.pagination.limit) { searchCriteria.pagination.limit = 10; } @@ -57,6 +56,11 @@ export const useCms = (rootContext: ApplicationVueContext): any => { ], }); + if (!searchCriteria.configuration.includes) { + // performance enhancement + searchCriteria.configuration.includes = getPageIncludes(); + } + try { const result = await getPage(path, searchCriteria, apiInstance); vuexStore.commit("SET_PAGE", result); diff --git a/packages/composables/src/internalHelpers/includesParameter.ts b/packages/composables/src/internalHelpers/includesParameter.ts new file mode 100644 index 000000000..d143129ae --- /dev/null +++ b/packages/composables/src/internalHelpers/includesParameter.ts @@ -0,0 +1,59 @@ +/** + * A collection of performance set of includes parameters + */ + +const PRODUCT = [ + "name", + "ratingAverage", + "calculatedPrice", + "calculatedPrices", + "cover", + "id", + "translated", + "options", +]; +const PRODUCT_MEDIA = ["media"]; +const PRODUCT_GROUP = ["id", "name", "options", "translated"]; +const PRODUCT_GROUP_OPTION = ["name", "id", "group", "translated"]; +const PRODUCT_CALCULATED_PRICE = ["unitPrice"]; +const MEDIA = ["url"]; +const CMS_PAGE = ["id", "name", "sections", "type", "config"]; +const CMS_PAGE_SECTION = [ + "id", + "backgroundMedia", + "blocks", + "type", + "sizingMode", +]; +const CMS_PAGE_BLOCK = [ + "slots", + "type", + "id", + "backgroundMedia", + "sectionPosition", +]; +const CMS_PAGE_SLOT = [ + "id", + "type", + "slot", + "blockId", + "data", + "backgroundMediaMode", + "backgroundMedia", +]; + +/** + * Parameters for page resolver - aligned with getPage method of @shopware-pwa/shopware-6-client + */ +export const getPageIncludes = () => ({ + cms_page_slot: CMS_PAGE_SLOT, + cms_page_block: CMS_PAGE_BLOCK, + cms_page_section: CMS_PAGE_SECTION, + cms_page: CMS_PAGE, + product: PRODUCT, + product_media: PRODUCT_MEDIA, + media: MEDIA, + calculated_price: PRODUCT_CALCULATED_PRICE, + product_group_option: PRODUCT_GROUP_OPTION, + product_group: PRODUCT_GROUP, +}); diff --git a/packages/shopware-6-client/__tests__/helpers/convertSearchCriteria.spec.ts b/packages/shopware-6-client/__tests__/helpers/convertSearchCriteria.spec.ts index d1ab93e55..13e190bec 100644 --- a/packages/shopware-6-client/__tests__/helpers/convertSearchCriteria.spec.ts +++ b/packages/shopware-6-client/__tests__/helpers/convertSearchCriteria.spec.ts @@ -456,6 +456,34 @@ describe("SearchConverter - convertSearchCriteria", () => { }); expect(result).toHaveProperty("associations"); }); + }); + describe("includes", () => { + it("should return includes object", () => { + const result = convertSearchCriteria({ + searchCriteria: { + configuration: { + includes: {}, + }, + }, + config, + }); + expect(result?.includes).toEqual({}); + }); + + it("should have product property within includes params", () => { + const result = convertSearchCriteria({ + searchCriteria: { + configuration: { + includes: { + product: ["name", "id"], + }, + }, + }, + config, + }); + expect(result).toHaveProperty("includes"); + expect(result.includes).toHaveProperty("product"); + }); it("should return multiple associations", () => { const result = convertSearchCriteria({ diff --git a/packages/shopware-6-client/src/helpers/searchConverter.ts b/packages/shopware-6-client/src/helpers/searchConverter.ts index 32a4856e0..7648acf8b 100644 --- a/packages/shopware-6-client/src/helpers/searchConverter.ts +++ b/packages/shopware-6-client/src/helpers/searchConverter.ts @@ -1,6 +1,7 @@ import { SearchCriteria, ShopwareSearchParams, + Includes, } from "@shopware-pwa/commons/interfaces/search/SearchCriteria"; import { NotFilter, @@ -43,6 +44,7 @@ export interface ShopwareParams { grouping?: Grouping; properties?: string; // store-api filters manufacturer?: string; // store-api filters + includes?: Includes; } /** @@ -138,6 +140,10 @@ export const convertSearchCriteria = ({ params.grouping = configuration.grouping; } + if (configuration?.includes) { + params.includes = configuration.includes; + } + // fulltext query (works for every entity so can be global) if (term) { params.term = term;