@laioutr/app-shopware 0.14.5 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -0
- package/dist/module.d.mts +15 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- package/dist/runtime/server/media-libraries/shopware.js +3 -3
- package/dist/runtime/server/middleware/defineShopware.js +13 -2
- package/dist/runtime/server/orchestr/category/base.resolver.js +9 -4
- package/dist/runtime/server/orchestr/category/bySlug.query.js +1 -1
- package/dist/runtime/server/orchestr/category/listing-page.page-index.js +17 -14
- package/dist/runtime/server/orchestr/menu/byAlias.query.js +6 -1
- package/dist/runtime/server/orchestr/product/base.resolver.js +5 -1
- package/dist/runtime/server/orchestr/product/byCategoryId.query.js +1 -1
- package/dist/runtime/server/orchestr/product/byCategorySlug.query.js +10 -7
- package/dist/runtime/server/orchestr/product/byCategorySlug.template.js +1 -1
- package/dist/runtime/server/orchestr/product/bySlug.query.js +1 -1
- package/dist/runtime/server/orchestr/product/detail-page.page-index.js +5 -3
- package/dist/runtime/server/orchestr/product/variants.link.js +5 -1
- package/dist/runtime/server/orchestr/product-variant/base.resolver.js +3 -1
- package/dist/runtime/server/orchestr/review/base.resolver.js +6 -3
- package/dist/runtime/server/orchestr-helper/requestedFields.d.ts +3 -16
- package/dist/runtime/server/orchestr-helper/requestedFields.js +46 -103
- package/dist/runtime/server/shopware-helper/criteria.d.ts +11 -0
- package/dist/runtime/server/shopware-helper/criteria.js +14 -0
- package/dist/runtime/server/shopware-helper/fetchAllProductVariants.d.ts +3 -1
- package/dist/runtime/server/shopware-helper/fetchAllProductVariants.js +4 -3
- package/dist/runtime/server/shopware-helper/shopwareSettings.d.ts +2 -0
- package/dist/runtime/server/shopware-helper/shopwareSettings.js +16 -0
- package/dist/runtime/server/shopware-helper/useSeoResolver.d.ts +2 -1
- package/dist/runtime/server/shopware-helper/useSeoResolver.js +2 -6
- package/dist/runtime/server/types/criteria.d.ts +7 -0
- package/dist/runtime/server/types/criteria.js +0 -0
- package/dist/runtime/server/types/settings.d.ts +21 -0
- package/dist/runtime/server/types/settings.js +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -71,6 +71,93 @@ export default defineNuxtConfig({
|
|
|
71
71
|
in the private runtime config, and only `storefrontUrl`'s origin is exposed publicly. Read them
|
|
72
72
|
from the environment rather than committing them to `nuxt.config.ts`.
|
|
73
73
|
|
|
74
|
+
In a Laioutr project the same options come from the app's `config` object in `laioutrrc.json`,
|
|
75
|
+
which the platform passes into this module — the `nuxt.config.ts` form above is for the playground
|
|
76
|
+
and for standalone Nuxt apps.
|
|
77
|
+
|
|
78
|
+
```jsonc
|
|
79
|
+
// laioutrrc.json
|
|
80
|
+
{
|
|
81
|
+
"apps": [
|
|
82
|
+
{
|
|
83
|
+
"name": "@laioutr/app-shopware",
|
|
84
|
+
"version": "0.14.5",
|
|
85
|
+
"config": { "endpoint": "https://shop.example.com/store-api" }
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Tuning the Store API reads
|
|
92
|
+
|
|
93
|
+
What the app asks Shopware for, and what those reads cost, is reachable through two Nitro hooks
|
|
94
|
+
rather than through config. A project's config is on its way to being editable from Studio, and
|
|
95
|
+
none of this is an editor's decision: a wrong `includes` value empties an entity component with no
|
|
96
|
+
error to trace it back from, and `maxLimit` describes the shop's deployment.
|
|
97
|
+
|
|
98
|
+
Both are **filter** hooks, seeded with what the app would otherwise have used — register no
|
|
99
|
+
handler and every read behaves exactly as it shipped.
|
|
100
|
+
|
|
101
|
+
### `shopware:criteria:resolve`
|
|
102
|
+
|
|
103
|
+
Fires once per store-API read that hydrates a canonical entity, carrying the projection and
|
|
104
|
+
relations for that read.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
// server/plugins/shopware-criteria.ts
|
|
108
|
+
export default defineNitroPlugin((nitro) => {
|
|
109
|
+
nitro.hooks.hook('shopware:criteria:resolve', ({ target, result }) => {
|
|
110
|
+
if (target !== 'product') return;
|
|
111
|
+
|
|
112
|
+
result.criteria.includes.product.push('customFields');
|
|
113
|
+
result.criteria.associations.properties = { associations: { group: {} } };
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
| `target` | Read |
|
|
119
|
+
| --- | --- |
|
|
120
|
+
| `product` | A product read, composed — the variant branch nested under it has already been through `product-variant`. |
|
|
121
|
+
| `product-variant` | A standalone variant read *and* the branch nested inside a product read, so widening variants widens them everywhere. |
|
|
122
|
+
| `category` | The category component resolver's read. |
|
|
123
|
+
| `menu` | The navigation read behind `MenuByAliasQuery`. |
|
|
124
|
+
| `product-review` | The review component resolver's read. |
|
|
125
|
+
|
|
126
|
+
Reads that hydrate nothing have no target and never fire: the id-only listing queries, both page
|
|
127
|
+
indexes, the breadcrumb link and the child-category link. Their payloads never reach a mapper.
|
|
128
|
+
|
|
129
|
+
`category` and `menu` arrive with an empty `includes` and stay that way unless a handler fills it —
|
|
130
|
+
Shopware returns whole rows when a read projects nothing. Putting a field there turns the read into
|
|
131
|
+
a whitelist, which is a way to make it *smaller*, not larger.
|
|
132
|
+
|
|
133
|
+
### `shopware:settings:resolve`
|
|
134
|
+
|
|
135
|
+
Fires once per Orchestr request, carrying what the reads cost and how far they reach.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
// server/plugins/shopware-settings.ts
|
|
139
|
+
export default defineNitroPlugin((nitro) => {
|
|
140
|
+
nitro.hooks.hook('shopware:settings:resolve', ({ result }) => {
|
|
141
|
+
result.settings.maxLimit = 25;
|
|
142
|
+
result.settings.totalCountMode = 'next-pages';
|
|
143
|
+
result.settings.catalog.menuDepth = 3;
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
| Setting | Default | Purpose |
|
|
149
|
+
| --- | --- | --- |
|
|
150
|
+
| `maxLimit` | `100` | The shop's `api.max_limit`. Page-index walks and the media library's folder read are clamped to it; a larger `limit` is rejected with a 400. |
|
|
151
|
+
| `totalCountMode` | `exact` | `total-count-mode` for product listing reads. `next-pages` is markedly cheaper on a large catalog but leaves the total an estimate. |
|
|
152
|
+
| `loadVariantsOnListing` | `true` | Pre-load every variant of every product on a category listing. Off, the variant resolver fetches on demand: smaller listing payloads, one extra read where the listing itself renders a variant picker. |
|
|
153
|
+
| `queryTemplateLimit` | `50` | How many categories the Studio query-template picker offers. |
|
|
154
|
+
| `mediaFolderLimit` | `500` | Page size for the media library's folder reads. |
|
|
155
|
+
| `catalog.menuDepth` | unset | Navigation depth for menu reads. Left unset, Shopware applies its own default of two levels. |
|
|
156
|
+
| `catalog.categoryPageIndex.types` | `['page']` | Shopware category `type` values the listing-page index covers. Add `'landing_page'` for shops that serve landing pages as listing pages. |
|
|
157
|
+
| `catalog.categoryPageIndex.minLevel` | `1` | Only categories *deeper* than this level are indexed — the default drops the navigation roots the storefront renders as the home page and the footer menu. |
|
|
158
|
+
| `catalog.categoryPageIndex.activeOnly` | `true` | Restrict the index to active categories. |
|
|
159
|
+
| `catalog.seoRouteNames` | `frontend.detail.page`; `frontend.navigation.page`, `frontend.landing.page` | `seo_url.routeName` values the slug resolver accepts, per entity type. Extend it when a plugin serves detail or listing pages under its own route name. |
|
|
160
|
+
|
|
74
161
|
## Checkout
|
|
75
162
|
|
|
76
163
|
The app registers a `Checkout` page type. Tag one page with it in Studio and drop the
|
package/dist/module.d.mts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { HookResult, NuxtModule } from '@nuxt/schema';
|
|
2
|
+
import { ShopwareCriteriaTarget, ShopwareCriteria } from '../dist/runtime/server/types/criteria.js';
|
|
3
|
+
import { ShopwareSettings } from '../dist/runtime/server/types/settings.js';
|
|
2
4
|
import { H3Event } from 'h3';
|
|
3
5
|
|
|
4
6
|
declare module 'vue' {
|
|
@@ -36,6 +38,19 @@ declare module 'nitropack' {
|
|
|
36
38
|
event: H3Event;
|
|
37
39
|
token: string | null;
|
|
38
40
|
}) => HookResult;
|
|
41
|
+
'shopware:criteria:resolve': (args: {
|
|
42
|
+
event: H3Event;
|
|
43
|
+
target: ShopwareCriteriaTarget;
|
|
44
|
+
result: {
|
|
45
|
+
criteria: ShopwareCriteria;
|
|
46
|
+
};
|
|
47
|
+
}) => HookResult;
|
|
48
|
+
'shopware:settings:resolve': (args: {
|
|
49
|
+
event: H3Event;
|
|
50
|
+
result: {
|
|
51
|
+
settings: ShopwareSettings;
|
|
52
|
+
};
|
|
53
|
+
}) => HookResult;
|
|
39
54
|
}
|
|
40
55
|
}
|
|
41
56
|
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { CHECKOUT_ENDPOINT_PATH, ADOPT_SESSION_ENDPOINT_PATH } from '../dist/run
|
|
|
4
4
|
import { registerLaioutrApp } from '@laioutr-core/kit';
|
|
5
5
|
|
|
6
6
|
const name = "@laioutr/app-shopware";
|
|
7
|
-
const version = "0.
|
|
7
|
+
const version = "0.15.0";
|
|
8
8
|
|
|
9
9
|
const module$1 = defineNuxtModule({
|
|
10
10
|
meta: {
|
|
@@ -38,10 +38,10 @@ export const buildShopwareMediaFilters = (query) => {
|
|
|
38
38
|
}
|
|
39
39
|
return filters;
|
|
40
40
|
};
|
|
41
|
-
const fetchChildFolders = async (api, parentId) => {
|
|
41
|
+
const fetchChildFolders = async (api, parentId, limit) => {
|
|
42
42
|
const response = await api.invoke("searchMediaFolder post /search/media-folder", {
|
|
43
43
|
body: {
|
|
44
|
-
limit
|
|
44
|
+
limit,
|
|
45
45
|
filter: [{ type: "equals", field: "parentId", value: parentId ?? null }],
|
|
46
46
|
sort: [{ field: "name", order: "ASC" }]
|
|
47
47
|
}
|
|
@@ -69,7 +69,7 @@ export default defineShopware.mediaLibrary({
|
|
|
69
69
|
...query.term ? { query: generateSearchFilter(query.term) } : {}
|
|
70
70
|
}
|
|
71
71
|
});
|
|
72
|
-
const folders = query.cursor || query.scope === "all" || query.term ? void 0 : await fetchChildFolders(api, query.folderId);
|
|
72
|
+
const folders = query.cursor || query.scope === "all" || query.term ? void 0 : await fetchChildFolders(api, query.folderId, ctx.settings.mediaFolderLimit);
|
|
73
73
|
const items = response.data.data?.map((media) => ({
|
|
74
74
|
media: mapMedia(media),
|
|
75
75
|
previewUrl: media.thumbnails?.[0]?.url ?? media.url ?? "",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { defineOrchestr } from "#imports";
|
|
1
|
+
import { defineOrchestr, useNitroApp } from "#imports";
|
|
2
2
|
import { shopwareAdminClientFactory } from "../client/shopwareAdminClientFactory.js";
|
|
3
3
|
import { shopwareClientFactory } from "../client/shopwareClientFactory.js";
|
|
4
|
+
import { defaultShopwareSettings } from "../shopware-helper/shopwareSettings.js";
|
|
4
5
|
import { getCurrentSystemEntities } from "../shopware-helper/system/getCurrentSystemEntities.js";
|
|
5
6
|
import { getCachedSystemEntities } from "../shopware-helper/system/getSystemEntities.js";
|
|
6
7
|
export const defineShopware = defineOrchestr.meta({
|
|
@@ -10,10 +11,18 @@ export const defineShopware = defineOrchestr.meta({
|
|
|
10
11
|
}).extendRequest(async (args) => {
|
|
11
12
|
const storefrontClient = await shopwareClientFactory(args.event);
|
|
12
13
|
const adminClient = shopwareAdminClientFactory();
|
|
14
|
+
const nitro = useNitroApp();
|
|
13
15
|
const systemEntities = await getCachedSystemEntities(storefrontClient);
|
|
14
16
|
const currentSystemEntities = getCurrentSystemEntities(systemEntities, args.clientEnv);
|
|
15
17
|
storefrontClient.defaultHeaders["sw-currency-id"] = currentSystemEntities.currency.id;
|
|
16
18
|
storefrontClient.defaultHeaders["sw-language-id"] = currentSystemEntities.locale.languageId;
|
|
19
|
+
const settings = { settings: defaultShopwareSettings() };
|
|
20
|
+
await nitro.hooks.callHook("shopware:settings:resolve", { event: args.event, result: settings });
|
|
21
|
+
const resolveCriteria = async (target, criteria) => {
|
|
22
|
+
const result = { criteria };
|
|
23
|
+
await nitro.hooks.callHook("shopware:criteria:resolve", { event: args.event, target, result });
|
|
24
|
+
return result.criteria;
|
|
25
|
+
};
|
|
17
26
|
return {
|
|
18
27
|
context: {
|
|
19
28
|
storefrontClient,
|
|
@@ -21,7 +30,9 @@ export const defineShopware = defineOrchestr.meta({
|
|
|
21
30
|
systemEntities,
|
|
22
31
|
currentSystemEntities,
|
|
23
32
|
/** The systems current currency iso code */
|
|
24
|
-
swCurrency: currentSystemEntities.currency.iso
|
|
33
|
+
swCurrency: currentSystemEntities.currency.iso,
|
|
34
|
+
settings: settings.settings,
|
|
35
|
+
resolveCriteria
|
|
25
36
|
}
|
|
26
37
|
};
|
|
27
38
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CategoryBase, CategoryContent, CategoryMedia, CategorySeo } from "@laioutr-core/canonical-types/entity/category";
|
|
2
2
|
import { categoriesToken } from "../../const/passthroughTokens.js";
|
|
3
3
|
import { defineShopwareComponentResolver } from "../../middleware/defineShopware.js";
|
|
4
|
+
import { toRequestCriteria } from "../../shopware-helper/criteria.js";
|
|
4
5
|
import { entitySlug } from "../../shopware-helper/mappers/slugMapper.js";
|
|
5
6
|
import { mapMedia } from "../../shopware-helper/mediaMapper.js";
|
|
6
7
|
import { swTranslated } from "../../shopware-helper/swTranslated.js";
|
|
@@ -10,10 +11,14 @@ export default defineShopwareComponentResolver({
|
|
|
10
11
|
provides: [CategoryBase, CategoryContent, CategoryMedia, CategorySeo],
|
|
11
12
|
resolve: async ({ entityIds, context, passthrough, $entity }) => {
|
|
12
13
|
const { storefrontClient } = context;
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
const readCategories = async () => {
|
|
15
|
+
const criteria = await context.resolveCriteria("category", { includes: {}, associations: { seoUrls: {}, media: {} } });
|
|
16
|
+
const response = await storefrontClient.invoke("readCategoryList post /category", {
|
|
17
|
+
body: { ids: entityIds, ...toRequestCriteria(criteria) }
|
|
18
|
+
});
|
|
19
|
+
return response.data.elements;
|
|
20
|
+
};
|
|
21
|
+
const categories = passthrough.has(categoriesToken) ? passthrough.get(categoriesToken) : await readCategories();
|
|
17
22
|
if (!categories) {
|
|
18
23
|
throw new Error(
|
|
19
24
|
"Categories not found in passthrough. The component resolver does not request categories from shopware at the moment."
|
|
@@ -3,7 +3,7 @@ import { defineShopwareQuery } from "../../middleware/defineShopware.js";
|
|
|
3
3
|
import { useSeoResolver } from "../../shopware-helper/useSeoResolver.js";
|
|
4
4
|
export default defineShopwareQuery(CategoryBySlugQuery, async ({ context, input }) => {
|
|
5
5
|
const { slug } = input;
|
|
6
|
-
const seoResolver = useSeoResolver(context.storefrontClient);
|
|
6
|
+
const seoResolver = useSeoResolver(context.storefrontClient, context.settings.catalog.seoRouteNames);
|
|
7
7
|
const seoEntry = await seoResolver.resolve("category", slug);
|
|
8
8
|
if (!seoEntry) {
|
|
9
9
|
throw new Error(`No seo url found for category slug: ${slug}`);
|
|
@@ -5,22 +5,22 @@ import { toCategoryPageEntry } from "../../shopware-helper/pageIndexEntries.js";
|
|
|
5
5
|
import { readCategoryPageMeta } from "../../shopware-helper/readCategoryPageMeta.js";
|
|
6
6
|
import { SHOPWARE_MAX_LIMIT, storeApiPageFetcher } from "../../shopware-helper/storeApiPageFetcher.js";
|
|
7
7
|
import { useSeoResolver } from "../../shopware-helper/useSeoResolver.js";
|
|
8
|
-
const
|
|
9
|
-
{ type: "
|
|
10
|
-
{ type: "equals", field: "active", value: true },
|
|
11
|
-
{ type: "range", field: "level", parameters: { gt:
|
|
8
|
+
const buildMembershipFilter = ({ categoryPageIndex }) => [
|
|
9
|
+
{ type: "equalsAny", field: "type", value: categoryPageIndex.types },
|
|
10
|
+
...categoryPageIndex.activeOnly ? [{ type: "equals", field: "active", value: true }] : [],
|
|
11
|
+
{ type: "range", field: "level", parameters: { gt: categoryPageIndex.minLevel } }
|
|
12
12
|
];
|
|
13
|
-
const
|
|
13
|
+
const buildPageIndexCriteria = (catalog) => ({
|
|
14
14
|
associations: { seoUrls: {}, media: {} },
|
|
15
15
|
includes: {
|
|
16
16
|
category: ["id", "name", "translated", "updatedAt", "seoUrls", "media"],
|
|
17
17
|
seo_url: ["seoPathInfo", "isCanonical", "routeName"],
|
|
18
18
|
media: ["url"]
|
|
19
19
|
},
|
|
20
|
-
filter:
|
|
20
|
+
filter: buildMembershipFilter(catalog),
|
|
21
21
|
// Page-number cursors need a stable upstream order, or a resumed walk shifts entries between passes.
|
|
22
22
|
sort: [{ field: "id", order: "ASC" }]
|
|
23
|
-
};
|
|
23
|
+
});
|
|
24
24
|
export default defineShopwarePageIndex({
|
|
25
25
|
for: ProductListingPage,
|
|
26
26
|
label: "Shopware Category",
|
|
@@ -37,7 +37,10 @@ export default defineShopwarePageIndex({
|
|
|
37
37
|
* locale would claim the category has no page in any other.
|
|
38
38
|
*/
|
|
39
39
|
locate: async ({ context, params }) => {
|
|
40
|
-
const seoEntry = await useSeoResolver(context.storefrontClient).resolve(
|
|
40
|
+
const seoEntry = await useSeoResolver(context.storefrontClient, context.settings.catalog.seoRouteNames).resolve(
|
|
41
|
+
"category",
|
|
42
|
+
params.slug
|
|
43
|
+
);
|
|
41
44
|
if (!seoEntry) return void 0;
|
|
42
45
|
return {
|
|
43
46
|
subject: { type: "Category", id: seoEntry.id },
|
|
@@ -46,16 +49,16 @@ export default defineShopwarePageIndex({
|
|
|
46
49
|
},
|
|
47
50
|
count: async ({ context }) => {
|
|
48
51
|
const response = await context.storefrontClient.invoke("readCategoryList post /category", {
|
|
49
|
-
body: { ...
|
|
52
|
+
body: { ...buildPageIndexCriteria(context.settings.catalog), limit: 1, "total-count-mode": "exact" }
|
|
50
53
|
});
|
|
51
54
|
return response.data.total ?? 0;
|
|
52
55
|
},
|
|
53
56
|
/** Substring match on name and metaTitle, the same form the query-template picker uses for categories. */
|
|
54
57
|
search: ({ context, term, take }) => context.storefrontClient.invoke("readCategoryList post /category", {
|
|
55
58
|
body: {
|
|
56
|
-
...
|
|
59
|
+
...buildPageIndexCriteria(context.settings.catalog),
|
|
57
60
|
filter: [
|
|
58
|
-
...
|
|
61
|
+
...buildMembershipFilter(context.settings.catalog),
|
|
59
62
|
{
|
|
60
63
|
type: "multi",
|
|
61
64
|
operator: "or",
|
|
@@ -65,14 +68,14 @@ export default defineShopwarePageIndex({
|
|
|
65
68
|
]
|
|
66
69
|
}
|
|
67
70
|
],
|
|
68
|
-
limit: take
|
|
71
|
+
limit: Math.min(take, context.settings.maxLimit)
|
|
69
72
|
}
|
|
70
73
|
}).then((response) => (response.data.elements ?? []).map(toCategoryPageEntry)),
|
|
71
74
|
list: ({ context, batchSize, startCursor }) => paginate(
|
|
72
75
|
storeApiPageFetcher(
|
|
73
|
-
({ page, limit }) => context.storefrontClient.invoke("readCategoryList post /category", { body: { ...
|
|
76
|
+
({ page, limit }) => context.storefrontClient.invoke("readCategoryList post /category", { body: { ...buildPageIndexCriteria(context.settings.catalog), page, limit } }).then((response) => response.data),
|
|
74
77
|
toCategoryPageEntry,
|
|
75
|
-
batchSize
|
|
78
|
+
Math.min(batchSize, context.settings.maxLimit)
|
|
76
79
|
),
|
|
77
80
|
startCursor
|
|
78
81
|
)
|
|
@@ -2,16 +2,21 @@ import { MenuByAliasQuery } from "@laioutr-core/canonical-types/ecommerce";
|
|
|
2
2
|
import { categoriesToken } from "../../const/passthroughTokens.js";
|
|
3
3
|
import { defineShopwareQuery } from "../../middleware/defineShopware.js";
|
|
4
4
|
import { flattenCategories } from "../../shopware-helper/categoryFlattener.js";
|
|
5
|
+
import { toRequestCriteria } from "../../shopware-helper/criteria.js";
|
|
5
6
|
export default defineShopwareQuery({
|
|
6
7
|
implements: MenuByAliasQuery,
|
|
7
8
|
run: async ({ input, context, passthrough }) => {
|
|
8
9
|
const { alias } = input;
|
|
10
|
+
const criteria = await context.resolveCriteria("menu", { includes: {}, associations: { seoUrls: {} } });
|
|
9
11
|
const response = await context.storefrontClient.invoke("readNavigation post /navigation/{activeId}/{rootId}", {
|
|
10
12
|
pathParams: {
|
|
11
13
|
activeId: alias,
|
|
12
14
|
rootId: alias
|
|
13
15
|
},
|
|
14
|
-
body: {
|
|
16
|
+
body: {
|
|
17
|
+
...toRequestCriteria(criteria),
|
|
18
|
+
...context.settings.catalog.menuDepth === void 0 ? {} : { depth: context.settings.catalog.menuDepth }
|
|
19
|
+
}
|
|
15
20
|
});
|
|
16
21
|
const flattenedCategories = flattenCategories(response.data ?? []);
|
|
17
22
|
passthrough.set(categoriesToken, flattenedCategories);
|
|
@@ -37,7 +37,11 @@ export default defineShopwareComponentResolver({
|
|
|
37
37
|
const loadedVariants = passthrough.get(productVariantsToken) ?? [];
|
|
38
38
|
const missingVariantIds = variantIds.filter((id) => !loadedVariants.some((variant) => variant.id === id));
|
|
39
39
|
if (missingVariantIds.length > 0) {
|
|
40
|
-
const response = await fetchAllProducts(context.storefrontClient, {
|
|
40
|
+
const response = await fetchAllProducts(context.storefrontClient, {
|
|
41
|
+
productIds: missingVariantIds,
|
|
42
|
+
loadVariants: false,
|
|
43
|
+
resolveCriteria: context.resolveCriteria
|
|
44
|
+
});
|
|
41
45
|
loadedVariants.push(...response);
|
|
42
46
|
}
|
|
43
47
|
const shopwareProducts = variantIds.map((id) => {
|
|
@@ -22,7 +22,7 @@ export default defineShopwareQuery(
|
|
|
22
22
|
includes: {
|
|
23
23
|
product: ["id", "parentId"]
|
|
24
24
|
},
|
|
25
|
-
"total-count-mode":
|
|
25
|
+
"total-count-mode": context.settings.totalCountMode,
|
|
26
26
|
"min-price": swBuiltInFilters?.["min-price"],
|
|
27
27
|
"max-price": swBuiltInFilters?.["max-price"],
|
|
28
28
|
manufacturer: swBuiltInFilters?.manufacturer,
|
|
@@ -13,7 +13,7 @@ export default defineShopwareQuery(
|
|
|
13
13
|
ProductsByCategorySlugQuery,
|
|
14
14
|
async ({ context, input, pagination, filter: selectedFilters, sorting, passthrough }) => {
|
|
15
15
|
const { categorySlug } = input;
|
|
16
|
-
const seoResolver = useSeoResolver(context.storefrontClient);
|
|
16
|
+
const seoResolver = useSeoResolver(context.storefrontClient, context.settings.catalog.seoRouteNames);
|
|
17
17
|
const seoEntry = await seoResolver.resolve("category", categorySlug);
|
|
18
18
|
if (!seoEntry) {
|
|
19
19
|
throw new Error(`No seo url found for category slug: ${categorySlug}`);
|
|
@@ -29,7 +29,7 @@ export default defineShopwareQuery(
|
|
|
29
29
|
includes: {
|
|
30
30
|
product: ["id", "parentId"]
|
|
31
31
|
},
|
|
32
|
-
"total-count-mode":
|
|
32
|
+
"total-count-mode": context.settings.totalCountMode,
|
|
33
33
|
"min-price": swBuiltInFilters?.["min-price"],
|
|
34
34
|
"max-price": swBuiltInFilters?.["max-price"],
|
|
35
35
|
manufacturer: swBuiltInFilters?.manufacturer,
|
|
@@ -43,11 +43,14 @@ export default defineShopwareQuery(
|
|
|
43
43
|
);
|
|
44
44
|
passthrough.set(parentIdToDefaultVariantIdToken, parentIdToDefaultVariantId);
|
|
45
45
|
cacheProductParentIds(response.data.elements.map((product) => [product.id, product.parentId ?? product.id]));
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
if (context.settings.loadVariantsOnListing) {
|
|
47
|
+
const allVariants = await fetchAllProducts(context.storefrontClient, {
|
|
48
|
+
productIds: Object.keys(parentIdToDefaultVariantId),
|
|
49
|
+
loadVariants: true,
|
|
50
|
+
resolveCriteria: context.resolveCriteria
|
|
51
|
+
});
|
|
52
|
+
passthrough.set(productVariantsToken, allVariants);
|
|
53
|
+
}
|
|
51
54
|
return {
|
|
52
55
|
// Return the parent-id, in case the received product is a variant
|
|
53
56
|
ids: response.data.elements.map((product) => product.parentId ?? product.id),
|
|
@@ -23,7 +23,7 @@ export default defineShopwareQueryTemplateProvider({
|
|
|
23
23
|
category: ["id", "name", "translated", "seoUrls"],
|
|
24
24
|
seo_url: ["seoPathInfo", "isCanonical", "routeName"]
|
|
25
25
|
},
|
|
26
|
-
limit:
|
|
26
|
+
limit: context.settings.queryTemplateLimit
|
|
27
27
|
}
|
|
28
28
|
});
|
|
29
29
|
const templates = [];
|
|
@@ -4,7 +4,7 @@ import { parentIdToDefaultVariantIdToken } from "../../const/passthroughTokens.j
|
|
|
4
4
|
import { defineShopwareQuery } from "../../middleware/defineShopware.js";
|
|
5
5
|
import { useSeoResolver } from "../../shopware-helper/useSeoResolver.js";
|
|
6
6
|
export default defineShopwareQuery(ProductBySlugQuery, async ({ context, input, passthrough }) => {
|
|
7
|
-
const seoResolver = useSeoResolver(context.storefrontClient);
|
|
7
|
+
const seoResolver = useSeoResolver(context.storefrontClient, context.settings.catalog.seoRouteNames);
|
|
8
8
|
const seoEntry = await seoResolver.resolve("product", input.slug);
|
|
9
9
|
if (!seoEntry) {
|
|
10
10
|
throw new Error(`No product found for slug: ${input.slug}`);
|
|
@@ -34,7 +34,7 @@ export default defineShopwarePageIndex({
|
|
|
34
34
|
*/
|
|
35
35
|
locate: async ({ context, params }) => {
|
|
36
36
|
const client = context.storefrontClient;
|
|
37
|
-
const seoEntry = await useSeoResolver(client).resolve("product", params.slug);
|
|
37
|
+
const seoEntry = await useSeoResolver(client, context.settings.catalog.seoRouteNames).resolve("product", params.slug);
|
|
38
38
|
if (!seoEntry) return void 0;
|
|
39
39
|
const parentId = await useGetProductParentId(client)(seoEntry.id);
|
|
40
40
|
const productId = parentId ?? seoEntry.id;
|
|
@@ -46,12 +46,14 @@ export default defineShopwarePageIndex({
|
|
|
46
46
|
});
|
|
47
47
|
return response.data.total ?? 0;
|
|
48
48
|
},
|
|
49
|
-
search: ({ context, term, take }) => context.storefrontClient.invoke("readProduct post /product", {
|
|
49
|
+
search: ({ context, term, take }) => context.storefrontClient.invoke("readProduct post /product", {
|
|
50
|
+
body: { ...pageIndexCriteria, term, limit: Math.min(take, context.settings.maxLimit) }
|
|
51
|
+
}).then((response) => (response.data.elements ?? []).map(toProductPageEntry)),
|
|
50
52
|
list: ({ context, batchSize, startCursor }) => paginate(
|
|
51
53
|
storeApiPageFetcher(
|
|
52
54
|
({ page, limit }) => context.storefrontClient.invoke("readProduct post /product", { body: { ...pageIndexCriteria, page, limit } }).then((response) => response.data),
|
|
53
55
|
toProductPageEntry,
|
|
54
|
-
batchSize
|
|
56
|
+
Math.min(batchSize, context.settings.maxLimit)
|
|
55
57
|
),
|
|
56
58
|
startCursor
|
|
57
59
|
)
|
|
@@ -3,7 +3,11 @@ import { productVariantsToken } from "../../const/passthroughTokens.js";
|
|
|
3
3
|
import { defineShopwareLink } from "../../middleware/defineShopware.js";
|
|
4
4
|
import { fetchAllProducts } from "../../shopware-helper/fetchAllProductVariants.js";
|
|
5
5
|
export default defineShopwareLink(ProductVariantsLink, async ({ entityIds, context, passthrough }) => {
|
|
6
|
-
const allVariants = passthrough.get(productVariantsToken) ?? await fetchAllProducts(context.storefrontClient, {
|
|
6
|
+
const allVariants = passthrough.get(productVariantsToken) ?? await fetchAllProducts(context.storefrontClient, {
|
|
7
|
+
productIds: entityIds,
|
|
8
|
+
loadVariants: true,
|
|
9
|
+
resolveCriteria: context.resolveCriteria
|
|
10
|
+
});
|
|
7
11
|
passthrough.set(productVariantsToken, allVariants);
|
|
8
12
|
return {
|
|
9
13
|
links: entityIds.map((productId) => {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { productVariantsToken } from "../../const/passthroughTokens.js";
|
|
13
13
|
import { defineShopwareComponentResolver } from "../../middleware/defineShopware.js";
|
|
14
14
|
import { resolveProductVariantFields } from "../../orchestr-helper/requestedFields.js";
|
|
15
|
+
import { toRequestCriteria } from "../../shopware-helper/criteria.js";
|
|
15
16
|
import { mapMedia } from "../../shopware-helper/mediaMapper.js";
|
|
16
17
|
import { swTranslated } from "../../shopware-helper/swTranslated.js";
|
|
17
18
|
export default defineShopwareComponentResolver({
|
|
@@ -32,10 +33,11 @@ export default defineShopwareComponentResolver({
|
|
|
32
33
|
const loadedVariants = passthrough.get(productVariantsToken) ?? [];
|
|
33
34
|
const missingVariantIds = entityIds.filter((id) => !loadedVariants.some((variant) => variant.id === id));
|
|
34
35
|
if (missingVariantIds.length > 0) {
|
|
36
|
+
const criteria = await resolveProductVariantFields(context.resolveCriteria);
|
|
35
37
|
const response = await context.storefrontClient.invoke("readProduct post /product", {
|
|
36
38
|
body: {
|
|
37
39
|
ids: missingVariantIds,
|
|
38
|
-
...
|
|
40
|
+
...toRequestCriteria(criteria)
|
|
39
41
|
}
|
|
40
42
|
});
|
|
41
43
|
loadedVariants.push(...response.data.elements ?? []);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ReviewBase } from "@laioutr-core/canonical-types/entity/review";
|
|
2
2
|
import { currentProductIdsToken } from "../../const/passthroughTokens.js";
|
|
3
3
|
import { defineShopwareComponentResolver } from "../../middleware/defineShopware.js";
|
|
4
|
+
import { toRequestCriteria } from "../../shopware-helper/criteria.js";
|
|
4
5
|
export default defineShopwareComponentResolver({
|
|
5
6
|
label: "Shopware Product Review Connector",
|
|
6
7
|
entityType: "Review",
|
|
@@ -9,14 +10,16 @@ export default defineShopwareComponentResolver({
|
|
|
9
10
|
const currentProductIds = passthrough.get(currentProductIdsToken);
|
|
10
11
|
if (!currentProductIds) throw new Error("Missing `currentProductIdsToken` in passthrough.");
|
|
11
12
|
let entities = [];
|
|
13
|
+
const criteria = await context.resolveCriteria("product-review", {
|
|
14
|
+
includes: { product_review: ["id", "title", "content", "points", "externalUser"] },
|
|
15
|
+
associations: {}
|
|
16
|
+
});
|
|
12
17
|
for (const productId of currentProductIds) {
|
|
13
18
|
const res = await context.storefrontClient.invoke("readProductReviews post /product/{productId}/reviews", {
|
|
14
19
|
pathParams: { productId },
|
|
15
20
|
body: {
|
|
16
21
|
ids: entityIds,
|
|
17
|
-
|
|
18
|
-
product_review: ["id", "title", "content", "points", "externalUser"]
|
|
19
|
-
}
|
|
22
|
+
...toRequestCriteria(criteria)
|
|
20
23
|
}
|
|
21
24
|
});
|
|
22
25
|
entities = entities.concat(...res.data.elements ?? []);
|
|
@@ -1,18 +1,5 @@
|
|
|
1
|
+
import type { ResolveCriteria, ShopwareCriteria } from '../types/criteria.js';
|
|
1
2
|
export declare const resolveProductFields: ({ loadVariants }: {
|
|
2
3
|
loadVariants: boolean;
|
|
3
|
-
}) =>
|
|
4
|
-
|
|
5
|
-
[key: string]: /*elided*/ any;
|
|
6
|
-
};
|
|
7
|
-
includes: {
|
|
8
|
-
[key: string]: string[];
|
|
9
|
-
};
|
|
10
|
-
};
|
|
11
|
-
export declare const resolveProductVariantFields: () => {
|
|
12
|
-
associations: {
|
|
13
|
-
[key: string]: /*elided*/ any;
|
|
14
|
-
};
|
|
15
|
-
includes: {
|
|
16
|
-
[key: string]: string[];
|
|
17
|
-
};
|
|
18
|
-
};
|
|
4
|
+
}, resolveCriteria: ResolveCriteria) => Promise<ShopwareCriteria>;
|
|
5
|
+
export declare const resolveProductVariantFields: (resolveCriteria: ResolveCriteria) => Promise<ShopwareCriteria>;
|
|
@@ -1,102 +1,49 @@
|
|
|
1
|
-
import { ProductVariantsLink } from "@laioutr-core/canonical-types/ecommerce";
|
|
2
1
|
import { MediaIncludes } from "../const/includes.js";
|
|
3
|
-
|
|
2
|
+
import { mergeIncludes } from "../shopware-helper/criteria.js";
|
|
4
3
|
const addAssociation = (name, add, association = {}) => add ? { [name]: association } : {};
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
...requestedComponents.includes("media") ? { cover: { associations: { media: {} } }, media: { associations: { media: {} } } } : {},
|
|
13
|
-
// gallery images (via product_media -> media)
|
|
14
|
-
...requestedComponents.includes("options") ? { options: { associations: { group: {} } } } : {},
|
|
15
|
-
// variant options like Color/Size + their group names
|
|
16
|
-
...requestedVariants ? {
|
|
17
|
-
children: {
|
|
18
|
-
associations: variantFields?.associations ?? {}
|
|
19
|
-
}
|
|
20
|
-
} : {}
|
|
21
|
-
};
|
|
22
|
-
const includes = {
|
|
23
|
-
product: unique([
|
|
24
|
-
"id",
|
|
25
|
-
"parentId",
|
|
26
|
-
...requestedComponents.includes("base") ? ["name", "seoUrls"] : [],
|
|
27
|
-
...requestedComponents.includes("info") ? ["name", "productNumber", "ean", "translated", "manufacturer", "description", "cover"] : [],
|
|
28
|
-
...requestedComponents.includes("description") ? ["translated", "description"] : [],
|
|
29
|
-
...requestedComponents.includes("seo") ? ["metaTitle", "metaDescription", "seoUrls"] : [],
|
|
30
|
-
...requestedComponents.includes("media") ? ["cover", "media"] : [],
|
|
31
|
-
...requestedComponents.includes("prices") ? ["minPurchase", "purchaseSteps", "maxPurchase", "calculatedPrice", "calculatedPrices"] : [],
|
|
32
|
-
...requestedLinks["ecommerce/product/variants"] ? ["children"] : [],
|
|
33
|
-
...variantFields?.includes?.product ?? []
|
|
34
|
-
]),
|
|
35
|
-
product_media: requestedComponents.includes("media") ? ["id", "mediaId", "media"] : [],
|
|
36
|
-
media: requestedComponents.includes("media") ? MediaIncludes : [],
|
|
37
|
-
property_group_option: requestedComponents.includes("availability") ? ["id", "name", "group"] : [],
|
|
38
|
-
property_group: requestedComponents.includes("availability") ? ["id", "name"] : []
|
|
39
|
-
};
|
|
40
|
-
return { associations, includes };
|
|
41
|
-
};
|
|
42
|
-
const mergeIncludes = (includes1, includes2) => {
|
|
43
|
-
const merged = {};
|
|
44
|
-
for (const [key, value] of Object.entries(includes1)) {
|
|
45
|
-
merged[key] = [...value];
|
|
46
|
-
}
|
|
47
|
-
for (const [key, value] of Object.entries(includes2)) {
|
|
48
|
-
merged[key] = [...merged[key] ?? [], ...value];
|
|
49
|
-
}
|
|
50
|
-
for (const [key, value] of Object.entries(merged)) {
|
|
51
|
-
merged[key] = unique([...value]);
|
|
52
|
-
}
|
|
53
|
-
return merged;
|
|
54
|
-
};
|
|
55
|
-
export const resolveProductFields = ({ loadVariants }) => {
|
|
56
|
-
const variantFields = loadVariants ? resolveProductVariantFields() : void 0;
|
|
57
|
-
const associations = {
|
|
58
|
-
cover: { associations: { media: {} } },
|
|
59
|
-
media: { associations: { media: {} } },
|
|
60
|
-
...addAssociation("children", loadVariants, variantFields?.associations ? { associations: variantFields?.associations } : {})
|
|
61
|
-
};
|
|
62
|
-
const includes = mergeIncludes(
|
|
63
|
-
{
|
|
64
|
-
product: [
|
|
65
|
-
"id",
|
|
66
|
-
"parentId",
|
|
67
|
-
"name",
|
|
68
|
-
"seoUrls",
|
|
69
|
-
"productNumber",
|
|
70
|
-
"ean",
|
|
71
|
-
"translated",
|
|
72
|
-
"manufacturer",
|
|
73
|
-
"description",
|
|
74
|
-
"cover",
|
|
75
|
-
"metaTitle",
|
|
76
|
-
"metaDescription",
|
|
77
|
-
"cover",
|
|
78
|
-
"media",
|
|
79
|
-
"minPurchase",
|
|
80
|
-
"purchaseSteps",
|
|
81
|
-
"maxPurchase",
|
|
82
|
-
"calculatedPrice",
|
|
83
|
-
"calculatedPrices",
|
|
84
|
-
"children",
|
|
85
|
-
"ratingAverage",
|
|
86
|
-
"productReviews"
|
|
87
|
-
],
|
|
88
|
-
product_media: ["id", "mediaId", "media"],
|
|
89
|
-
media: MediaIncludes
|
|
4
|
+
export const resolveProductFields = async ({ loadVariants }, resolveCriteria) => {
|
|
5
|
+
const variantFields = loadVariants ? await resolveProductVariantFields(resolveCriteria) : void 0;
|
|
6
|
+
return resolveCriteria("product", {
|
|
7
|
+
associations: {
|
|
8
|
+
cover: { associations: { media: {} } },
|
|
9
|
+
media: { associations: { media: {} } },
|
|
10
|
+
...addAssociation("children", loadVariants, variantFields ? { associations: variantFields.associations } : {})
|
|
90
11
|
},
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
12
|
+
includes: mergeIncludes(
|
|
13
|
+
{
|
|
14
|
+
product: [
|
|
15
|
+
"id",
|
|
16
|
+
"parentId",
|
|
17
|
+
"name",
|
|
18
|
+
"seoUrls",
|
|
19
|
+
"productNumber",
|
|
20
|
+
"ean",
|
|
21
|
+
"translated",
|
|
22
|
+
"manufacturer",
|
|
23
|
+
"description",
|
|
24
|
+
"cover",
|
|
25
|
+
"metaTitle",
|
|
26
|
+
"metaDescription",
|
|
27
|
+
"cover",
|
|
28
|
+
"media",
|
|
29
|
+
"minPurchase",
|
|
30
|
+
"purchaseSteps",
|
|
31
|
+
"maxPurchase",
|
|
32
|
+
"calculatedPrice",
|
|
33
|
+
"calculatedPrices",
|
|
34
|
+
"children",
|
|
35
|
+
"ratingAverage",
|
|
36
|
+
"productReviews"
|
|
37
|
+
],
|
|
38
|
+
product_media: ["id", "mediaId", "media"],
|
|
39
|
+
media: MediaIncludes
|
|
40
|
+
},
|
|
41
|
+
variantFields?.includes ?? {}
|
|
42
|
+
)
|
|
43
|
+
});
|
|
97
44
|
};
|
|
98
|
-
export const resolveProductVariantFields = () => {
|
|
99
|
-
|
|
45
|
+
export const resolveProductVariantFields = async (resolveCriteria) => resolveCriteria("product-variant", {
|
|
46
|
+
associations: {
|
|
100
47
|
cover: { associations: { media: {} } },
|
|
101
48
|
// main image
|
|
102
49
|
media: { associations: { media: {} } },
|
|
@@ -106,8 +53,8 @@ export const resolveProductVariantFields = () => {
|
|
|
106
53
|
manufacturer: {},
|
|
107
54
|
deliveryTime: {},
|
|
108
55
|
prices: { associations: { rule: {} } }
|
|
109
|
-
}
|
|
110
|
-
|
|
56
|
+
},
|
|
57
|
+
includes: {
|
|
111
58
|
product: [
|
|
112
59
|
"id",
|
|
113
60
|
"parentId",
|
|
@@ -137,9 +84,5 @@ export const resolveProductVariantFields = () => {
|
|
|
137
84
|
media: MediaIncludes,
|
|
138
85
|
property_group_option: ["id", "name", "group", "translated"],
|
|
139
86
|
property_group: ["id", "name", "translated"]
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
associations,
|
|
143
|
-
includes
|
|
144
|
-
};
|
|
145
|
-
};
|
|
87
|
+
}
|
|
88
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ShopwareCriteria } from '../types/criteria.js';
|
|
2
|
+
import type { ShopwareIncludesQuery } from '../types/shopware.js';
|
|
3
|
+
export declare const mergeIncludes: (...projections: ShopwareIncludesQuery[]) => ShopwareIncludesQuery;
|
|
4
|
+
export declare const toRequestCriteria: ({ includes, associations }: ShopwareCriteria) => {
|
|
5
|
+
associations?: {
|
|
6
|
+
[key: string]: /*elided*/ any;
|
|
7
|
+
} | undefined;
|
|
8
|
+
includes?: {
|
|
9
|
+
[key: string]: string[];
|
|
10
|
+
} | undefined;
|
|
11
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const unique = (arr) => Array.from(new Set(arr));
|
|
2
|
+
export const mergeIncludes = (...projections) => {
|
|
3
|
+
const merged = {};
|
|
4
|
+
for (const projection of projections) {
|
|
5
|
+
for (const [entity, fields] of Object.entries(projection)) {
|
|
6
|
+
merged[entity] = unique([...merged[entity] ?? [], ...fields]);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
return merged;
|
|
10
|
+
};
|
|
11
|
+
export const toRequestCriteria = ({ includes, associations }) => ({
|
|
12
|
+
...Object.keys(includes).length > 0 ? { includes } : {},
|
|
13
|
+
...Object.keys(associations).length > 0 ? { associations } : {}
|
|
14
|
+
});
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import type { ResolveCriteria } from '../types/criteria.js';
|
|
1
2
|
import { StorefrontClient } from '../types/shopware.js';
|
|
2
|
-
export declare const fetchAllProducts: (storefrontClient: StorefrontClient, { productIds, loadVariants }: {
|
|
3
|
+
export declare const fetchAllProducts: (storefrontClient: StorefrontClient, { productIds, loadVariants, resolveCriteria }: {
|
|
3
4
|
productIds: string[];
|
|
4
5
|
loadVariants: boolean;
|
|
6
|
+
resolveCriteria: ResolveCriteria;
|
|
5
7
|
}) => Promise<{
|
|
6
8
|
active?: boolean;
|
|
7
9
|
apiAlias: "product";
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { toRequestCriteria } from "./criteria.js";
|
|
1
2
|
import { resolveProductFields } from "../orchestr-helper/requestedFields.js";
|
|
2
|
-
export const fetchAllProducts = async (storefrontClient, { productIds, loadVariants }) => {
|
|
3
|
-
const fields = resolveProductFields({ loadVariants });
|
|
3
|
+
export const fetchAllProducts = async (storefrontClient, { productIds, loadVariants, resolveCriteria }) => {
|
|
4
4
|
if (productIds.length === 0) {
|
|
5
5
|
return [];
|
|
6
6
|
}
|
|
7
|
+
const criteria = await resolveProductFields({ loadVariants }, resolveCriteria);
|
|
7
8
|
const response = await storefrontClient.invoke("readProduct post /product", {
|
|
8
9
|
body: {
|
|
9
10
|
ids: productIds,
|
|
10
|
-
...
|
|
11
|
+
...toRequestCriteria(criteria)
|
|
11
12
|
}
|
|
12
13
|
});
|
|
13
14
|
const products = response.data.elements ?? [];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SHOPWARE_MAX_LIMIT } from "./storeApiPageFetcher.js";
|
|
2
|
+
export const defaultShopwareSettings = () => ({
|
|
3
|
+
maxLimit: SHOPWARE_MAX_LIMIT,
|
|
4
|
+
totalCountMode: "exact",
|
|
5
|
+
loadVariantsOnListing: true,
|
|
6
|
+
queryTemplateLimit: 50,
|
|
7
|
+
mediaFolderLimit: 500,
|
|
8
|
+
catalog: {
|
|
9
|
+
menuDepth: void 0,
|
|
10
|
+
categoryPageIndex: { types: ["page"], minLevel: 1, activeOnly: true },
|
|
11
|
+
seoRouteNames: {
|
|
12
|
+
product: ["frontend.detail.page"],
|
|
13
|
+
category: ["frontend.navigation.page", "frontend.landing.page"]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ShopwareCatalogSettings } from '../types/settings.js';
|
|
1
2
|
import { StorefrontClient } from '../types/shopware.js';
|
|
2
3
|
type SeoUrlType = 'product' | 'category';
|
|
3
4
|
interface SeoEntry {
|
|
@@ -5,7 +6,7 @@ interface SeoEntry {
|
|
|
5
6
|
id: string;
|
|
6
7
|
matchedPath: string;
|
|
7
8
|
}
|
|
8
|
-
export declare const useSeoResolver: (storefrontClient: StorefrontClient) => {
|
|
9
|
+
export declare const useSeoResolver: (storefrontClient: StorefrontClient, routeNames: ShopwareCatalogSettings["seoRouteNames"]) => {
|
|
9
10
|
resolve: (type: SeoUrlType, slug: string) => Promise<SeoEntry | undefined>;
|
|
10
11
|
};
|
|
11
12
|
export {};
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { extractShopwareId, isShopwareId } from "./isShopwareId.js";
|
|
2
2
|
import { isSlugMatchingSeoPath } from "./mappers/slugMapper.js";
|
|
3
3
|
import { useUserlandCache } from "#imports";
|
|
4
|
-
const typeToRouteNames = {
|
|
5
|
-
product: ["frontend.detail.page"],
|
|
6
|
-
category: ["frontend.navigation.page", "frontend.landing.page"]
|
|
7
|
-
};
|
|
8
4
|
const SEO_ENTRY_TTL = 60 * 60 * 24;
|
|
9
|
-
export const useSeoResolver = (storefrontClient) => {
|
|
5
|
+
export const useSeoResolver = (storefrontClient, routeNames) => {
|
|
10
6
|
const cache = useUserlandCache("shopware/seo-urls");
|
|
11
7
|
const resolve = async (type, slug) => {
|
|
12
8
|
const languageId = storefrontClient.defaultHeaders["sw-language-id"] ?? "default";
|
|
@@ -38,7 +34,7 @@ export const useSeoResolver = (storefrontClient) => {
|
|
|
38
34
|
{ field: "seoPathInfo", type: "equals", value: slug }
|
|
39
35
|
]
|
|
40
36
|
},
|
|
41
|
-
{ field: "routeName", type: "equalsAny", value:
|
|
37
|
+
{ field: "routeName", type: "equalsAny", value: routeNames[type] }
|
|
42
38
|
]
|
|
43
39
|
}
|
|
44
40
|
]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ShopwareAssociationsQuery, ShopwareIncludesQuery } from './shopware.js';
|
|
2
|
+
export type ShopwareCriteriaTarget = 'product' | 'product-variant' | 'category' | 'menu' | 'product-review';
|
|
3
|
+
export interface ShopwareCriteria {
|
|
4
|
+
includes: ShopwareIncludesQuery;
|
|
5
|
+
associations: ShopwareAssociationsQuery;
|
|
6
|
+
}
|
|
7
|
+
export type ResolveCriteria = (target: ShopwareCriteriaTarget, criteria: ShopwareCriteria) => Promise<ShopwareCriteria>;
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Schemas } from './storeApiTypes.js';
|
|
2
|
+
export interface ShopwareCatalogSettings {
|
|
3
|
+
menuDepth: number | undefined;
|
|
4
|
+
categoryPageIndex: {
|
|
5
|
+
types: string[];
|
|
6
|
+
minLevel: number;
|
|
7
|
+
activeOnly: boolean;
|
|
8
|
+
};
|
|
9
|
+
seoRouteNames: {
|
|
10
|
+
product: string[];
|
|
11
|
+
category: string[];
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export interface ShopwareSettings {
|
|
15
|
+
maxLimit: number;
|
|
16
|
+
totalCountMode: Schemas['TotalCountMode'];
|
|
17
|
+
loadVariantsOnListing: boolean;
|
|
18
|
+
queryTemplateLimit: number;
|
|
19
|
+
mediaFolderLimit: number;
|
|
20
|
+
catalog: ShopwareCatalogSettings;
|
|
21
|
+
}
|
|
File without changes
|