Przeglądaj źródła

feat(core): Add collectionIds and collectionSlugs filters to default search plugin (#3945)

David Höck 2 miesięcy temu
rodzic
commit
82fcf0f1ba

+ 2 - 0
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -5936,7 +5936,9 @@ export type ScheduledTask = {
 
 export type SearchInput = {
   collectionId?: InputMaybe<Scalars['ID']['input']>;
+  collectionIds?: InputMaybe<Array<Scalars['ID']['input']>>;
   collectionSlug?: InputMaybe<Scalars['String']['input']>;
+  collectionSlugs?: InputMaybe<Array<Scalars['String']['input']>>;
   facetValueFilters?: InputMaybe<Array<FacetValueFilterInput>>;
   /** @deprecated Use `facetValueFilters` instead */
   facetValueIds?: InputMaybe<Array<Scalars['ID']['input']>>;

+ 2 - 0
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -5526,7 +5526,9 @@ export type ScheduledTask = {
 
 export type SearchInput = {
     collectionId?: InputMaybe<Scalars['ID']['input']>;
+    collectionIds?: InputMaybe<Array<Scalars['ID']['input']>>;
     collectionSlug?: InputMaybe<Scalars['String']['input']>;
+    collectionSlugs?: InputMaybe<Array<Scalars['String']['input']>>;
     facetValueFilters?: InputMaybe<Array<FacetValueFilterInput>>;
     /** @deprecated Use `facetValueFilters` instead */
     facetValueIds?: InputMaybe<Array<Scalars['ID']['input']>>;

+ 2 - 0
packages/common/src/generated-shop-types.ts

@@ -3153,7 +3153,9 @@ export type RoleList = PaginatedList & {
 
 export type SearchInput = {
     collectionId?: InputMaybe<Scalars['ID']['input']>;
+    collectionIds?: InputMaybe<Array<Scalars['ID']['input']>>;
     collectionSlug?: InputMaybe<Scalars['String']['input']>;
+    collectionSlugs?: InputMaybe<Array<Scalars['String']['input']>>;
     facetValueFilters?: InputMaybe<Array<FacetValueFilterInput>>;
     /** @deprecated Use `facetValueFilters` instead */
     facetValueIds?: InputMaybe<Array<Scalars['ID']['input']>>;

+ 2 - 0
packages/common/src/generated-types.ts

@@ -5858,7 +5858,9 @@ export type ScheduledTask = {
 
 export type SearchInput = {
   collectionId?: InputMaybe<Scalars['ID']['input']>;
+  collectionIds?: InputMaybe<Array<Scalars['ID']['input']>>;
   collectionSlug?: InputMaybe<Scalars['String']['input']>;
+  collectionSlugs?: InputMaybe<Array<Scalars['String']['input']>>;
   facetValueFilters?: InputMaybe<Array<FacetValueFilterInput>>;
   /** @deprecated Use `facetValueFilters` instead */
   facetValueIds?: InputMaybe<Array<Scalars['ID']['input']>>;

+ 102 - 0
packages/core/e2e/default-search-plugin.e2e-spec.ts

@@ -395,6 +395,92 @@ describe('Default search plugin', () => {
         ]);
     }
 
+    async function testMatchCollectionIds(testProducts: TestProducts) {
+        const result = await testProducts({
+            collectionIds: ['T_2', 'T_3'],
+            groupByProduct: true,
+        });
+
+        // Should return products from both Plants (T_2) and Electronics (T_3) collections
+        // Plants has 3 products in the default test data
+        expect(result.search.items.length).toBeGreaterThanOrEqual(3);
+        expect(result.search.totalItems).toBeGreaterThanOrEqual(3);
+
+        const productNames = result.search.items.map(i => i.productName);
+
+        // Verify that products from Plants collection are included
+        expect(productNames).toContain('Bonsai Tree');
+        expect(productNames).toContain('Orchid');
+        expect(productNames).toContain('Spiky Cactus');
+    }
+
+    async function testMatchCollectionSlugs(testProducts: TestProducts) {
+        const result = await testProducts({
+            collectionSlugs: ['plants', 'electronics'],
+            groupByProduct: true,
+        });
+
+        // Should return products from both Plants and Electronics collections
+        // Plants has 3 products in the default test data
+        expect(result.search.items.length).toBeGreaterThanOrEqual(3);
+        expect(result.search.totalItems).toBeGreaterThanOrEqual(3);
+
+        const productNames = result.search.items.map(i => i.productName);
+
+        // Verify that products from Plants collection are included
+        expect(productNames).toContain('Bonsai Tree');
+        expect(productNames).toContain('Orchid');
+        expect(productNames).toContain('Spiky Cactus');
+    }
+
+    async function testCollectionIdsEdgeCases(testProducts: TestProducts) {
+        // Test with duplicate IDs - should handle gracefully
+        const resultWithDuplicates = await testProducts({
+            collectionIds: ['T_2', 'T_2', 'T_2'],
+            groupByProduct: true,
+        });
+
+        // Should still return Plants collection products, de-duplicated
+        expect(resultWithDuplicates.search.items.map(i => i.productName).sort()).toEqual([
+            'Bonsai Tree',
+            'Orchid',
+            'Spiky Cactus',
+        ]);
+
+        // Test with non-existent collection ID - should return no results
+        const resultNonExistent = await testProducts({
+            collectionIds: ['T_999'],
+            groupByProduct: true,
+        });
+
+        expect(resultNonExistent.search.items).toEqual([]);
+        expect(resultNonExistent.search.totalItems).toBe(0);
+    }
+
+    async function testCollectionSlugsEdgeCases(testProducts: TestProducts) {
+        // Test with duplicate slugs - should handle gracefully
+        const resultWithDuplicates = await testProducts({
+            collectionSlugs: ['plants', 'plants', 'plants'],
+            groupByProduct: true,
+        });
+
+        // Should still return Plants collection products, de-duplicated
+        expect(resultWithDuplicates.search.items.map(i => i.productName).sort()).toEqual([
+            'Bonsai Tree',
+            'Orchid',
+            'Spiky Cactus',
+        ]);
+
+        // Test with non-existent collection slug - should return no results
+        const resultNonExistent = await testProducts({
+            collectionSlugs: ['non-existent'],
+            groupByProduct: true,
+        });
+
+        expect(resultNonExistent.search.items).toEqual([]);
+        expect(resultNonExistent.search.totalItems).toBe(0);
+    }
+
     async function testSinglePrices(client: SimpleGraphQLClient) {
         const result = await client.query<SearchGetPricesQuery, SearchGetPricesQueryVariables>(
             SEARCH_GET_PRICES,
@@ -479,6 +565,14 @@ describe('Default search plugin', () => {
 
         it('matches by collectionSlug', () => testMatchCollectionSlug(testProductsShop));
 
+        it('matches by multiple collectionIds', () => testMatchCollectionIds(testProductsShop));
+
+        it('matches by multiple collectionSlugs', () => testMatchCollectionSlugs(testProductsShop));
+
+        it('handles collectionIds edge cases', () => testCollectionIdsEdgeCases(testProductsShop));
+
+        it('handles collectionSlugs edge cases', () => testCollectionSlugsEdgeCases(testProductsShop));
+
         it('single prices', () => testSinglePrices(shopClient));
 
         it('price ranges', () => testPriceRanges(shopClient));
@@ -779,6 +873,14 @@ describe('Default search plugin', () => {
 
         it('matches by collectionSlug', () => testMatchCollectionSlug(testProductsAdmin));
 
+        it('matches by multiple collectionIds', () => testMatchCollectionIds(testProductsAdmin));
+
+        it('matches by multiple collectionSlugs', () => testMatchCollectionSlugs(testProductsAdmin));
+
+        it('handles collectionIds edge cases', () => testCollectionIdsEdgeCases(testProductsAdmin));
+
+        it('handles collectionSlugs edge cases', () => testCollectionSlugsEdgeCases(testProductsAdmin));
+
         it('single prices', () => testSinglePrices(adminClient));
 
         it('price ranges', () => testPriceRanges(adminClient));

+ 2 - 0
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -5526,7 +5526,9 @@ export type ScheduledTask = {
 
 export type SearchInput = {
     collectionId?: InputMaybe<Scalars['ID']['input']>;
+    collectionIds?: InputMaybe<Array<Scalars['ID']['input']>>;
     collectionSlug?: InputMaybe<Scalars['String']['input']>;
+    collectionSlugs?: InputMaybe<Array<Scalars['String']['input']>>;
     facetValueFilters?: InputMaybe<Array<FacetValueFilterInput>>;
     /** @deprecated Use `facetValueFilters` instead */
     facetValueIds?: InputMaybe<Array<Scalars['ID']['input']>>;

+ 2 - 0
packages/core/e2e/graphql/generated-e2e-shop-types.ts

@@ -3044,7 +3044,9 @@ export type RoleList = PaginatedList & {
 
 export type SearchInput = {
     collectionId?: InputMaybe<Scalars['ID']['input']>;
+    collectionIds?: InputMaybe<Array<Scalars['ID']['input']>>;
     collectionSlug?: InputMaybe<Scalars['String']['input']>;
+    collectionSlugs?: InputMaybe<Array<Scalars['String']['input']>>;
     facetValueFilters?: InputMaybe<Array<FacetValueFilterInput>>;
     /** @deprecated Use `facetValueFilters` instead */
     facetValueIds?: InputMaybe<Array<Scalars['ID']['input']>>;

+ 34 - 2
packages/core/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts

@@ -159,8 +159,16 @@ export class MysqlSearchStrategy implements SearchStrategy {
         qb: SelectQueryBuilder<SearchIndexItem>,
         input: SearchInput,
     ): SelectQueryBuilder<SearchIndexItem> {
-        const { term, facetValueFilters, facetValueIds, facetValueOperator, collectionId, collectionSlug } =
-            input;
+        const {
+            term,
+            facetValueFilters,
+            facetValueIds,
+            facetValueOperator,
+            collectionId,
+            collectionSlug,
+            collectionIds,
+            collectionSlugs,
+        } = input;
 
         if (term && term.length > this.minTermLength) {
             const safeTerm = term
@@ -262,6 +270,30 @@ export class MysqlSearchStrategy implements SearchStrategy {
         if (collectionSlug) {
             qb.andWhere('FIND_IN_SET (:collectionSlug, si.collectionSlugs)', { collectionSlug });
         }
+        if (collectionIds?.length) {
+            qb.andWhere(
+                new Brackets(qb1 => {
+                    for (const id of Array.from(new Set(collectionIds))) {
+                        const placeholder = createPlaceholderFromId(id);
+                        qb1.orWhere(`FIND_IN_SET(:${placeholder}, si.collectionIds)`, {
+                            [placeholder]: id,
+                        });
+                    }
+                }),
+            );
+        }
+        if (collectionSlugs?.length) {
+            qb.andWhere(
+                new Brackets(qb1 => {
+                    for (const slug of Array.from(new Set(collectionSlugs))) {
+                        const placeholder = createPlaceholderFromId(slug);
+                        qb1.orWhere(`FIND_IN_SET(:${placeholder}, si.collectionSlugs)`, {
+                            [placeholder]: slug,
+                        });
+                    }
+                }),
+            );
+        }
 
         qb.andWhere('si.channelId = :channelId', { channelId: ctx.channelId });
         applyLanguageConstraints(qb, ctx.languageCode, ctx.channel.defaultLanguageCode);

+ 40 - 2
packages/core/src/plugin/default-search-plugin/search-strategy/postgres-search-strategy.ts

@@ -159,8 +159,16 @@ export class PostgresSearchStrategy implements SearchStrategy {
         input: SearchInput,
         forceGroup: boolean = false,
     ): SelectQueryBuilder<SearchIndexItem> {
-        const { term, facetValueFilters, facetValueIds, facetValueOperator, collectionId, collectionSlug } =
-            input;
+        const {
+            term,
+            facetValueFilters,
+            facetValueIds,
+            facetValueOperator,
+            collectionId,
+            collectionSlug,
+            collectionIds,
+            collectionSlugs,
+        } = input;
         // join multiple words with the logical AND operator
         const termLogicalAnd = term
             ? term
@@ -257,6 +265,36 @@ export class PostgresSearchStrategy implements SearchStrategy {
                 collectionSlug,
             });
         }
+        if (collectionIds?.length) {
+            qb.andWhere(
+                new Brackets(qb1 => {
+                    for (const id of Array.from(new Set(collectionIds))) {
+                        const placeholder = createPlaceholderFromId(id);
+                        qb1.orWhere(
+                            `:${placeholder}::varchar = ANY (string_to_array(si.collectionIds, ','))`,
+                            {
+                                [placeholder]: id,
+                            },
+                        );
+                    }
+                }),
+            );
+        }
+        if (collectionSlugs?.length) {
+            qb.andWhere(
+                new Brackets(qb1 => {
+                    for (const slug of Array.from(new Set(collectionSlugs))) {
+                        const placeholder = createPlaceholderFromId(slug);
+                        qb1.orWhere(
+                            `:${placeholder}::varchar = ANY (string_to_array(si.collectionSlugs, ','))`,
+                            {
+                                [placeholder]: slug,
+                            },
+                        );
+                    }
+                }),
+            );
+        }
 
         qb.andWhere('si.channelId = :channelId', { channelId: ctx.channelId });
         applyLanguageConstraints(qb, ctx.languageCode, ctx.channel.defaultLanguageCode);

+ 34 - 2
packages/core/src/plugin/default-search-plugin/search-strategy/sqlite-search-strategy.ts

@@ -150,8 +150,16 @@ export class SqliteSearchStrategy implements SearchStrategy {
         qb: SelectQueryBuilder<SearchIndexItem>,
         input: SearchInput,
     ): SelectQueryBuilder<SearchIndexItem> {
-        const { term, facetValueFilters, facetValueIds, facetValueOperator, collectionId, collectionSlug } =
-            input;
+        const {
+            term,
+            facetValueFilters,
+            facetValueIds,
+            facetValueOperator,
+            collectionId,
+            collectionSlug,
+            collectionIds,
+            collectionSlugs,
+        } = input;
 
         qb.where('1 = 1');
         if (term && term.length > this.minTermLength) {
@@ -237,6 +245,30 @@ export class SqliteSearchStrategy implements SearchStrategy {
                 collectionSlug: `%,${collectionSlug},%`,
             });
         }
+        if (collectionIds?.length) {
+            qb.andWhere(
+                new Brackets(qb1 => {
+                    for (const id of Array.from(new Set(collectionIds))) {
+                        const placeholder = createPlaceholderFromId(id);
+                        qb1.orWhere(`(',' || si.collectionIds || ',') LIKE :${placeholder}`, {
+                            [placeholder]: `%,${id},%`,
+                        });
+                    }
+                }),
+            );
+        }
+        if (collectionSlugs?.length) {
+            qb.andWhere(
+                new Brackets(qb1 => {
+                    for (const slug of Array.from(new Set(collectionSlugs))) {
+                        const placeholder = createPlaceholderFromId(slug);
+                        qb1.orWhere(`(',' || si.collectionSlugs || ',') LIKE :${placeholder}`, {
+                            [placeholder]: `%,${slug},%`,
+                        });
+                    }
+                }),
+            );
+        }
 
         qb.andWhere('si.channelId = :channelId', { channelId: ctx.channelId });
         applyLanguageConstraints(qb, ctx.languageCode, ctx.channel.defaultLanguageCode);

+ 99 - 17
packages/elasticsearch-plugin/e2e/e2e-helpers.ts

@@ -76,11 +76,9 @@ export async function testMatchSearchTerm(client: SimpleGraphQLClient) {
             },
         },
     );
-    expect(result.search.items.map(i => i.productName)).toEqual([
-        'Camera Lens',
-        'Instant Camera',
-        'SLR Camera',
-    ]);
+    expect(result.search.items.map(i => i.productName).sort((a, b) => a.localeCompare(b))).toEqual(
+        ['Camera Lens', 'Instant Camera', 'SLR Camera'].sort((a, b) => a.localeCompare(b)),
+    );
 }
 
 export async function testMatchFacetIdsAnd(client: SimpleGraphQLClient) {
@@ -150,8 +148,10 @@ export async function testMatchFacetValueFiltersAnd(client: SimpleGraphQLClient)
             },
         },
     );
-    expect(result.search.items.map(i => i.productName).sort()).toEqual(
-        ['Laptop', 'Curvy Monitor', 'Gaming PC', 'Hard Drive', 'Clacky Keyboard', 'USB Cable'].sort(),
+    expect(result.search.items.map(i => i.productName).sort((a, b) => a.localeCompare(b))).toEqual(
+        ['Laptop', 'Curvy Monitor', 'Gaming PC', 'Hard Drive', 'Clacky Keyboard', 'USB Cable'].sort((a, b) =>
+            a.localeCompare(b),
+        ),
     );
 }
 
@@ -169,7 +169,7 @@ export async function testMatchFacetValueFiltersOr(client: SimpleGraphQLClient)
             },
         },
     );
-    expect(result.search.items.map(i => i.productName).sort()).toEqual(
+    expect(result.search.items.map(i => i.productName).sort((a, b) => a.localeCompare(b))).toEqual(
         [
             'Bonsai Tree',
             'Bonsai Tree (Ch2)',
@@ -185,7 +185,7 @@ export async function testMatchFacetValueFiltersOr(client: SimpleGraphQLClient)
             'Spiky Cactus',
             'Tripod',
             'USB Cable',
-        ].sort(),
+        ].sort((a, b) => a.localeCompare(b)),
     );
 }
 
@@ -199,7 +199,7 @@ export async function testMatchFacetValueFiltersOrWithAnd(client: SimpleGraphQLC
             },
         },
     );
-    expect(result.search.items.map(i => i.productName).sort()).toEqual(
+    expect(result.search.items.map(i => i.productName).sort((a, b) => a.localeCompare(b))).toEqual(
         [
             'Laptop',
             'Curvy Monitor',
@@ -211,7 +211,7 @@ export async function testMatchFacetValueFiltersOrWithAnd(client: SimpleGraphQLC
             'Camera Lens',
             'Tripod',
             'SLR Camera',
-        ].sort(),
+        ].sort((a, b) => a.localeCompare(b)),
     );
 }
 
@@ -227,7 +227,7 @@ export async function testMatchFacetValueFiltersWithFacetIdsOr(client: SimpleGra
             },
         },
     );
-    expect(result.search.items.map(i => i.productName).sort()).toEqual(
+    expect(result.search.items.map(i => i.productName).sort((a, b) => a.localeCompare(b))).toEqual(
         [
             'Laptop',
             'Curvy Monitor',
@@ -239,7 +239,7 @@ export async function testMatchFacetValueFiltersWithFacetIdsOr(client: SimpleGra
             'Camera Lens',
             'Tripod',
             'SLR Camera',
-        ].sort(),
+        ].sort((a, b) => a.localeCompare(b)),
     );
 }
 
@@ -255,8 +255,8 @@ export async function testMatchFacetValueFiltersWithFacetIdsAnd(client: SimpleGr
             },
         },
     );
-    expect(result.search.items.map(i => i.productName).sort()).toEqual(
-        ['Instant Camera', 'Camera Lens', 'Tripod', 'SLR Camera'].sort(),
+    expect(result.search.items.map(i => i.productName).sort((a, b) => a.localeCompare(b))).toEqual(
+        ['Instant Camera', 'Camera Lens', 'Tripod', 'SLR Camera'].sort((a, b) => a.localeCompare(b)),
     );
 }
 
@@ -270,7 +270,7 @@ export async function testMatchCollectionId(client: SimpleGraphQLClient) {
             },
         },
     );
-    expect(result.search.items.map(i => i.productName).sort()).toEqual([
+    expect(result.search.items.map(i => i.productName).sort((a, b) => a.localeCompare(b))).toEqual([
         'Bonsai Tree',
         'Bonsai Tree (Ch2)',
         'Orchid',
@@ -288,7 +288,7 @@ export async function testMatchCollectionSlug(client: SimpleGraphQLClient) {
             },
         },
     );
-    expect(result.search.items.map(i => i.productName).sort()).toEqual([
+    expect(result.search.items.map(i => i.productName).sort((a, b) => a.localeCompare(b))).toEqual([
         'Bonsai Tree',
         'Bonsai Tree (Ch2)',
         'Orchid',
@@ -296,6 +296,88 @@ export async function testMatchCollectionSlug(client: SimpleGraphQLClient) {
     ]);
 }
 
+async function testMatchCollections(client: SimpleGraphQLClient, searchInput: Partial<SearchInput>) {
+    const result = await client.query<SearchProductsShopQuery, SearchProductsShopQueryVariables>(
+        SEARCH_PRODUCTS_SHOP,
+        {
+            input: {
+                groupByProduct: true,
+                ...searchInput,
+            },
+        },
+    );
+    // Should return products from both Plants (T_2) and Electronics (T_3) collections
+    expect(result.search.items.length).toBeGreaterThan(4);
+    expect(result.search.totalItems).toBeGreaterThan(4);
+
+    // Verify that products from both collections are included by checking collectionIds
+    const allCollectionIds = result.search.items.flatMap(i => i.collectionIds);
+
+    // Should contain products from Plants collection (T_2)
+    expect(allCollectionIds.filter(id => id === 'T_2').length).toBeGreaterThan(0);
+
+    // Should contain products from Electronics collection (T_3)
+    expect(allCollectionIds.filter(id => id === 'T_3').length).toBeGreaterThan(0);
+}
+
+export async function testMatchCollectionIds(client: SimpleGraphQLClient) {
+    return testMatchCollections(client, { collectionIds: ['T_2', 'T_3'] });
+}
+
+export async function testMatchCollectionSlugs(client: SimpleGraphQLClient) {
+    return testMatchCollections(client, { collectionSlugs: ['plants', 'electronics'] });
+}
+
+async function testCollectionEdgeCases(
+    client: SimpleGraphQLClient,
+    duplicateInput: Partial<SearchInput>,
+    nonExistentInput: Partial<SearchInput>,
+) {
+    // Test with duplicates - should handle gracefully
+    const resultWithDuplicates = await client.query<
+        SearchProductsShopQuery,
+        SearchProductsShopQueryVariables
+    >(SEARCH_PRODUCTS_SHOP, {
+        input: {
+            groupByProduct: true,
+            ...duplicateInput,
+        },
+    });
+    // Should still return Plants collection products, de-duplicated
+    expect(
+        resultWithDuplicates.search.items.map(i => i.productName).sort((a, b) => a.localeCompare(b)),
+    ).toEqual(['Bonsai Tree', 'Bonsai Tree (Ch2)', 'Orchid', 'Spiky Cactus']);
+
+    // Test with non-existent collection - should return no results
+    const resultNonExistent = await client.query<SearchProductsShopQuery, SearchProductsShopQueryVariables>(
+        SEARCH_PRODUCTS_SHOP,
+        {
+            input: {
+                groupByProduct: true,
+                ...nonExistentInput,
+            },
+        },
+    );
+    expect(resultNonExistent.search.items).toEqual([]);
+    expect(resultNonExistent.search.totalItems).toBe(0);
+}
+
+export async function testCollectionIdsEdgeCases(client: SimpleGraphQLClient) {
+    return testCollectionEdgeCases(
+        client,
+        { collectionIds: ['T_2', 'T_2', 'T_2'] },
+        { collectionIds: ['T_999'] },
+    );
+}
+
+export async function testCollectionSlugsEdgeCases(client: SimpleGraphQLClient) {
+    return testCollectionEdgeCases(
+        client,
+        { collectionSlugs: ['plants', 'plants', 'plants'] },
+        { collectionSlugs: ['non-existent-collection'] },
+    );
+}
+
 export async function testSinglePrices(client: SimpleGraphQLClient) {
     const result = await client.query<SearchGetPricesQuery, SearchGetPricesQueryVariables>(
         SEARCH_GET_PRICES,

+ 58 - 5
packages/elasticsearch-plugin/e2e/elasticsearch-plugin.e2e-spec.ts

@@ -3,11 +3,9 @@ import { CurrencyCode, SortOrder } from '@vendure/common/lib/generated-types';
 import { pick } from '@vendure/common/lib/pick';
 import {
     DefaultJobQueuePlugin,
-    DefaultLogger,
     FacetValue,
     facetValueCollectionFilter,
     LanguageCode,
-    LogLevel,
     mergeConfig,
 } from '@vendure/core';
 import { createTestEnvironment, E2E_DEFAULT_CHANNEL_TOKEN } from '@vendure/testing';
@@ -17,15 +15,15 @@ import path from 'path';
 import { afterAll, beforeAll, describe, expect, it } from 'vitest';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
-import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
+import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
 import * as Codegen from '../../core/e2e/graphql/generated-e2e-admin-types';
 import {
     SearchProductsShopQuery,
     SearchProductsShopQueryVariables,
 } from '../../core/e2e/graphql/generated-e2e-shop-types';
 import {
-    ASSIGN_PRODUCTVARIANT_TO_CHANNEL,
     ASSIGN_PRODUCT_TO_CHANNEL,
+    ASSIGN_PRODUCTVARIANT_TO_CHANNEL,
     CREATE_CHANNEL,
     CREATE_COLLECTION,
     CREATE_FACET,
@@ -34,8 +32,8 @@ import {
     DELETE_ASSET,
     DELETE_PRODUCT,
     DELETE_PRODUCT_VARIANT,
-    REMOVE_PRODUCTVARIANT_FROM_CHANNEL,
     REMOVE_PRODUCT_FROM_CHANNEL,
+    REMOVE_PRODUCTVARIANT_FROM_CHANNEL,
     UPDATE_ASSET,
     UPDATE_COLLECTION,
     UPDATE_PRODUCT,
@@ -49,10 +47,14 @@ import { ElasticsearchPlugin } from '../src/plugin';
 import {
     doAdminSearchQuery,
     dropElasticIndices,
+    testCollectionIdsEdgeCases,
+    testCollectionSlugsEdgeCases,
     testGroupByProduct,
     testGroupBySKU,
     testMatchCollectionId,
+    testMatchCollectionIds,
     testMatchCollectionSlug,
+    testMatchCollectionSlugs,
     testMatchFacetIdsAnd,
     testMatchFacetIdsOr,
     testMatchFacetValueFiltersAnd,
@@ -192,6 +194,39 @@ describe('Elasticsearch plugin', () => {
         // We have extra time here because a lot of jobs are
         // triggered from all the product updates
         await awaitRunningJobs(adminClient, 10_000, 1000);
+
+        // Create an Electronics collection for testing multi-collection filters
+        await adminClient.query<Codegen.CreateCollectionMutation, Codegen.CreateCollectionMutationVariables>(
+            CREATE_COLLECTION,
+            {
+                input: {
+                    translations: [
+                        {
+                            languageCode: LanguageCode.en,
+                            name: 'Electronics',
+                            description: 'Electronics products',
+                            slug: 'electronics',
+                        },
+                    ],
+                    filters: [
+                        {
+                            code: facetValueCollectionFilter.code,
+                            arguments: [
+                                {
+                                    name: 'facetValueIds',
+                                    value: '["T_1"]',
+                                },
+                                {
+                                    name: 'containsAny',
+                                    value: 'false',
+                                },
+                            ],
+                        },
+                    ],
+                },
+            },
+        );
+
         await adminClient.query(REINDEX);
         await awaitRunningJobs(adminClient);
     }, TEST_SETUP_TIMEOUT_MS);
@@ -230,6 +265,14 @@ describe('Elasticsearch plugin', () => {
 
         it('matches by collectionSlug', () => testMatchCollectionSlug(shopClient));
 
+        it('matches by multiple collectionIds', () => testMatchCollectionIds(shopClient));
+
+        it('matches by multiple collectionSlugs', () => testMatchCollectionSlugs(shopClient));
+
+        it('handles collectionIds edge cases', () => testCollectionIdsEdgeCases(shopClient));
+
+        it('handles collectionSlugs edge cases', () => testCollectionSlugsEdgeCases(shopClient));
+
         it('single prices', () => testSinglePrices(shopClient));
 
         it('price ranges', () => testPriceRanges(shopClient));
@@ -331,6 +374,7 @@ describe('Elasticsearch plugin', () => {
             });
             expect(result.search.collections).toEqual([
                 { collection: { id: 'T_2', name: 'Plants' }, count: 4 },
+                { collection: { id: 'T_3', name: 'Electronics' }, count: 21 },
             ]);
         });
 
@@ -345,6 +389,7 @@ describe('Elasticsearch plugin', () => {
             });
             expect(result.search.collections).toEqual([
                 { collection: { id: 'T_2', name: 'Plants' }, count: 4 },
+                { collection: { id: 'T_3', name: 'Electronics' }, count: 10 },
             ]);
         });
 
@@ -508,6 +553,14 @@ describe('Elasticsearch plugin', () => {
 
         it('matches by collectionSlug', () => testMatchCollectionSlug(adminClient));
 
+        it('matches by multiple collectionIds', () => testMatchCollectionIds(adminClient));
+
+        it('matches by multiple collectionSlugs', () => testMatchCollectionSlugs(adminClient));
+
+        it('handles collectionIds edge cases', () => testCollectionIdsEdgeCases(adminClient));
+
+        it('handles collectionSlugs edge cases', () => testCollectionSlugsEdgeCases(adminClient));
+
         it('single prices', () => testSinglePrices(adminClient));
 
         it('price ranges', () => testPriceRanges(adminClient));

+ 2 - 0
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -5526,7 +5526,9 @@ export type ScheduledTask = {
 
 export type SearchInput = {
     collectionId?: InputMaybe<Scalars['ID']['input']>;
+    collectionIds?: InputMaybe<Array<Scalars['ID']['input']>>;
     collectionSlug?: InputMaybe<Scalars['String']['input']>;
+    collectionSlugs?: InputMaybe<Array<Scalars['String']['input']>>;
     facetValueFilters?: InputMaybe<Array<FacetValueFilterInput>>;
     /** @deprecated Use `facetValueFilters` instead */
     facetValueIds?: InputMaybe<Array<Scalars['ID']['input']>>;

+ 2 - 0
packages/payments-plugin/e2e/graphql/generated-admin-types.ts

@@ -5613,7 +5613,9 @@ export type ScheduledTask = {
 
 export type SearchInput = {
     collectionId?: InputMaybe<Scalars['ID']['input']>;
+    collectionIds?: InputMaybe<Array<Scalars['ID']['input']>>;
     collectionSlug?: InputMaybe<Scalars['String']['input']>;
+    collectionSlugs?: InputMaybe<Array<Scalars['String']['input']>>;
     facetValueFilters?: InputMaybe<Array<FacetValueFilterInput>>;
     /** @deprecated Use `facetValueFilters` instead */
     facetValueIds?: InputMaybe<Array<Scalars['ID']['input']>>;

+ 2 - 0
packages/payments-plugin/e2e/graphql/generated-shop-types.ts

@@ -3130,7 +3130,9 @@ export type RoleList = PaginatedList & {
 
 export type SearchInput = {
     collectionId?: InputMaybe<Scalars['ID']['input']>;
+    collectionIds?: InputMaybe<Array<Scalars['ID']['input']>>;
     collectionSlug?: InputMaybe<Scalars['String']['input']>;
+    collectionSlugs?: InputMaybe<Array<Scalars['String']['input']>>;
     facetValueFilters?: InputMaybe<Array<FacetValueFilterInput>>;
     /** @deprecated Use `facetValueFilters` instead */
     facetValueIds?: InputMaybe<Array<Scalars['ID']['input']>>;

+ 2 - 0
packages/payments-plugin/src/mollie/graphql/generated-shop-types.ts

@@ -3244,7 +3244,9 @@ export type RoleList = PaginatedList & {
 
 export type SearchInput = {
     collectionId?: InputMaybe<Scalars['ID']['input']>;
+    collectionIds?: InputMaybe<Array<Scalars['ID']['input']>>;
     collectionSlug?: InputMaybe<Scalars['String']['input']>;
+    collectionSlugs?: InputMaybe<Array<Scalars['String']['input']>>;
     facetValueFilters?: InputMaybe<Array<FacetValueFilterInput>>;
     /** @deprecated Use `facetValueFilters` instead */
     facetValueIds?: InputMaybe<Array<Scalars['ID']['input']>>;

Plik diff jest za duży
+ 0 - 0
schema-admin.json


Plik diff jest za duży
+ 0 - 0
schema-shop.json


Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików