Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion orchestr-playground/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<main class="px-4 py-4">
<!-- Main Menu -->
<div class="mb-16 text-center">
<h1 class="mb-3 text-4xl font-bold text-gray-900">Nimstrata Playground</h1>
<h1 class="mb-3 text-4xl font-bold text-gray-900">Shopware Playground</h1>
<p class="text-xl text-gray-600">Choose a tool to test</p>
</div>

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"test:watch": "vitest watch"
},
"dependencies": {
"@laioutr-core/canonical-types": "^0.15.0",
"@laioutr-core/canonical-types": "^0.16.0",
"@laioutr-core/frontend-core": "^0.21.0",
"@laioutr-core/kit": "^0.7.9",
"@nuxt/kit": "3.16.2",
Expand Down
31 changes: 24 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions src/runtime/server/const/passthroughTokens.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createPassthroughToken } from '#imports';
import { PageTypeToken } from '@laioutr-core/canonical-types/page';
import { ShopwareCart, ShopwareCategory, ShopwareProduct } from '../types/shopware';

export const currentProductIdsToken = createPassthroughToken<string[]>('@laioutr/app-shopware/currentProductIdsFragment');
Expand All @@ -21,3 +22,15 @@ export const productsFragmentToken = createPassthroughToken<ShopwareProduct[]>('
export const productVariantsToken = createPassthroughToken<ShopwareProduct[]>('@laioutr/app-shopware/productVariants');

export const cartFragmentToken = createPassthroughToken<ShopwareCart>('@laioutr-app/shopify/cartFragment');

export const suggestionResultsFragmentToken = createPassthroughToken<{
id: string;
suggestions: Array<{
id: string;
type: string;
title: string;
link:
| { type: 'reference'; reference: { type: string; id: string; slug: string } }
| { type: 'pageType'; pageType: PageTypeToken; params: Record<string, string> };
}>;
}>('@laioutr/app-shopware/completionResults');
57 changes: 57 additions & 0 deletions src/runtime/server/orchestr/category/base.resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { CategoryBase, CategoryContent, CategoryMedia, CategorySeo } from '@laioutr-core/canonical-types/entity/category';
import { categoriesToken } from '../../const/passthroughTokens';
import { defineShopwareComponentResolver } from '../../middleware/defineShopware';
import { entitySlug } from '../../shopware-helper/mappers/slugMapper';
import { mapMedia } from '../../shopware-helper/mediaMapper';
import { swTranslated } from '../../shopware-helper/swTranslated';

export default defineShopwareComponentResolver({
entityType: 'Category',
label: 'Shopware Category Connector',
provides: [CategoryBase, CategoryContent, CategoryMedia, CategorySeo],
resolve: async ({ entityIds, context, passthrough, $entity }) => {
const { storefrontClient } = context;

const categories =
passthrough.has(categoriesToken) ?
passthrough.get(categoriesToken)!
: (await storefrontClient.invoke('readCategoryList post /category')).data.elements;

if (!categories) {
throw new Error(
'Categories not found in passthrough. The component resolver does not request categories from shopware at the moment.'
);
}

return {
entities: categories
.filter((category) => entityIds.includes(category.id))
.map((category) =>
$entity({
id: category.id,

base: () => ({
slug: entitySlug(category),
title: swTranslated(category, 'name') ?? category.name,
}),

content: () => ({
description: { html: swTranslated(category, 'description') ?? category.description ?? '' },
}),

media: () => ({
media: category.media ? [mapMedia(category.media)] : [],
}),

seo: () => ({
title: category.metaTitle,
description: category.metaDescription,
}),
})
),
};
},
cache: {
ttl: '10 minutes',
},
});
15 changes: 15 additions & 0 deletions src/runtime/server/orchestr/category/bySlug.query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { CategoryBySlugQuery } from '@laioutr-core/canonical-types/ecommerce';
import { defineShopwareQuery } from '../../middleware/defineShopware';
import { useSeoResolver } from '../../shopware-helper/useSeoResolver';

export default defineShopwareQuery(CategoryBySlugQuery, async ({ context, input }) => {
const { slug } = input;

const seoResolver = useSeoResolver(context.storefrontClient);
const seoEntry = await seoResolver.resolve('category', slug);
if (!seoEntry) {
throw new Error(`No seo url found for category slug: ${slug}`);
}

return { id: seoEntry.id };
});
2 changes: 1 addition & 1 deletion src/runtime/server/orchestr/product/search.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default defineShopwareQuery(ProductSearchQuery, async ({ context, input,

// Shopware API client exposes incorrect types for aggregations :<
const availableFilters = mapShopwareAggregationToAvailableFilters(response.data.aggregations as unknown as ShopwareAggregations);
const availableSortings = mapShopwareSortingToOrchestr(response.data.availableSortings);
const availableSortings = mapShopwareSortingToOrchestr(response.data.availableSortings ?? []);

// Tell the product-resolver which variants to use.
const parentIdToDefaultVariantId = Object.fromEntries(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { SuggestedSearchEntryBase } from '@laioutr-core/canonical-types/entity/suggested-search-entry';
import { suggestionResultsFragmentToken } from '../../const/passthroughTokens';
import { defineShopwareComponentResolver } from '../../middleware/defineShopware';

export default defineShopwareComponentResolver({
label: 'Shopware Suggested Search Entry Resolver',
entityType: 'SuggestedSearchEntry',
provides: [SuggestedSearchEntryBase],
resolve: ({ passthrough, $entity }) => {
const results = passthrough.require(suggestionResultsFragmentToken);

const entities = results.suggestions.map(({ id, type, title, link }) =>
$entity({
id,

base: () => ({
type,
title,
link,
}),
})
);

return { entities };
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { SuggestedSearchEntriesLink } from '@laioutr-core/canonical-types/suggested-search';
import { suggestionResultsFragmentToken } from '../../const/passthroughTokens';
import { defineShopwareLink } from '../../middleware/defineShopware';

export default defineShopwareLink(SuggestedSearchEntriesLink, async ({ passthrough }) => {
const results = passthrough.require(suggestionResultsFragmentToken);

return {
links: [
{
sourceId: results.id,
targetIds: results.suggestions.map((res) => res.id),
},
],
};
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { ProductSearchPage } from '@laioutr-core/canonical-types/ecommerce';
import { SuggestedSearchSearchQuery } from '@laioutr-core/canonical-types/suggested-search';
import { suggestionResultsFragmentToken } from '../../const/passthroughTokens';
import { defineShopwareQuery } from '../../middleware/defineShopware';
import { createFallbackSlug } from '../../shopware-helper/mappers/slugMapper';
import { ShopwareExtensions } from '../../types/shopware';

export default defineShopwareQuery(SuggestedSearchSearchQuery, async ({ context, input, passthrough }) => {
const { storefrontClient } = context;

const { query } = input;

const { data } = await storefrontClient.invoke('searchSuggest post /search-suggest', {
body: { search: query ?? '' },
});

const res = data as unknown as ShopwareExtensions;

const querySuggestionsResults = Object.values(res.extensions?.completionResult ?? {})
.filter((val) => val !== 'array_struct')
.map((val, index) => ({
id: `query_suggestion_${index}`,
type: 'query-suggestion',
title: val,
link: { type: 'pageType', pageType: ProductSearchPage, params: { q: val } } as const,
}));

const brandResults = Object.values(res.extensions?.multiSuggestResult?.suggestResults?.product_manufacturer?.elements ?? []).map(
(manufacturer) => ({
id: manufacturer.id,
type: 'brand',
title: manufacturer.name,
link: {
type: 'reference',
reference: { type: 'brand', id: manufacturer.id, slug: createFallbackSlug(manufacturer.name, manufacturer.id) },
} as const,
})
);

const categoryResults = Object.values(res.extensions?.multiSuggestResult?.suggestResults?.category?.elements ?? []).map((category) => ({
id: category.id,
type: 'category',
title: category.name,
link: {
type: 'reference',
reference: { type: 'category', id: category.id, slug: category.seoUrls?.[0]?.pathInfo?.split('/').at(-1) ?? '' },
} as const,
}));

const productResults = (data.elements ?? []).map((product) => ({
id: product.id,
type: 'product',
title: product.name,
link: {
type: 'reference',
reference: { type: 'product', id: product.id, slug: product.seoUrls?.[0]?.pathInfo?.split('/').at(-1) ?? '' },
} as const,
}));

const id = `search-suggest:${query}`;

passthrough.set(suggestionResultsFragmentToken, {
id,
suggestions: [...querySuggestionsResults, ...brandResults, ...categoryResults, ...productResults],
});

return { id };
});
12 changes: 12 additions & 0 deletions src/runtime/server/types/shopware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,15 @@ export type ShopwareAssociationsQuery = components['schemas']['Association'];
export type WithSeoUrl = {
seoUrls?: ShopwareSeoUrl[];
};

export type ShopwareExtensions = {
extensions?: {
completionResult?: Record<string, string>;
multiSuggestResult?: {
suggestResults?: {
product_manufacturer?: { elements: Record<string, ShopwareManufacturer> };
category?: { elements: Record<string, ShopwareCategory> };
};
};
};
};
Loading