Kaynağa Gözat

feat(core): Implement search by collection slug

Closes #405
Michael Bromley 5 yıl önce
ebeveyn
işleme
a4cbdbbd22

+ 5 - 4
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -607,7 +607,7 @@ export type CreateZoneInput = {
 /**
  * @description
  * ISO 4217 currency code
- * 
+ *
  * @docsCategory common
  */
 export enum CurrencyCode {
@@ -1407,7 +1407,7 @@ export type JobSortParameter = {
 /**
  * @description
  * The state of a Job in the JobQueue
- * 
+ *
  * @docsCategory common
  */
 export enum JobState {
@@ -1425,7 +1425,7 @@ export enum JobState {
  * region or script modifier (e.g. de_AT). The selection available is based
  * on the [Unicode CLDR summary list](https://unicode-org.github.io/cldr-staging/charts/37/summary/root.html)
  * and includes the major spoken languages of the world and any widely-used variants.
- * 
+ *
  * @docsCategory common
  */
 export enum LanguageCode {
@@ -2661,7 +2661,7 @@ export type PaymentMethodSortParameter = {
  * @description
  * Permissions for administrators and customers. Used to control access to
  * GraphQL resolvers via the {@link Allow} decorator.
- * 
+ *
  * @docsCategory common
  */
 export enum Permission {
@@ -3340,6 +3340,7 @@ export type SearchInput = {
   facetValueIds?: Maybe<Array<Scalars['ID']>>;
   facetValueOperator?: Maybe<LogicalOperator>;
   collectionId?: Maybe<Scalars['ID']>;
+  collectionSlug?: Maybe<Scalars['String']>;
   groupByProduct?: Maybe<Scalars['Boolean']>;
   take?: Maybe<Scalars['Int']>;
   skip?: Maybe<Scalars['Int']>;

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

@@ -3170,6 +3170,7 @@ export type SearchInput = {
     facetValueIds?: Maybe<Array<Scalars['ID']>>;
     facetValueOperator?: Maybe<LogicalOperator>;
     collectionId?: Maybe<Scalars['ID']>;
+    collectionSlug?: Maybe<Scalars['String']>;
     groupByProduct?: Maybe<Scalars['Boolean']>;
     take?: Maybe<Scalars['Int']>;
     skip?: Maybe<Scalars['Int']>;

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

@@ -2146,6 +2146,7 @@ export type SearchInput = {
     facetValueIds?: Maybe<Array<Scalars['ID']>>;
     facetValueOperator?: Maybe<LogicalOperator>;
     collectionId?: Maybe<Scalars['ID']>;
+    collectionSlug?: Maybe<Scalars['String']>;
     groupByProduct?: Maybe<Scalars['Boolean']>;
     take?: Maybe<Scalars['Int']>;
     skip?: Maybe<Scalars['Int']>;

+ 5 - 4
packages/common/src/generated-types.ts

@@ -606,7 +606,7 @@ export type CreateZoneInput = {
 /**
  * @description
  * ISO 4217 currency code
- * 
+ *
  * @docsCategory common
  */
 export enum CurrencyCode {
@@ -1399,7 +1399,7 @@ export type JobSortParameter = {
 /**
  * @description
  * The state of a Job in the JobQueue
- * 
+ *
  * @docsCategory common
  */
 export enum JobState {
@@ -1417,7 +1417,7 @@ export enum JobState {
  * region or script modifier (e.g. de_AT). The selection available is based
  * on the [Unicode CLDR summary list](https://unicode-org.github.io/cldr-staging/charts/37/summary/root.html)
  * and includes the major spoken languages of the world and any widely-used variants.
- * 
+ *
  * @docsCategory common
  */
 export enum LanguageCode {
@@ -2621,7 +2621,7 @@ export type PaymentMethodSortParameter = {
  * @description
  * Permissions for administrators and customers. Used to control access to
  * GraphQL resolvers via the {@link Allow} decorator.
- * 
+ *
  * @docsCategory common
  */
 export enum Permission {
@@ -3297,6 +3297,7 @@ export type SearchInput = {
   facetValueIds?: Maybe<Array<Scalars['ID']>>;
   facetValueOperator?: Maybe<LogicalOperator>;
   collectionId?: Maybe<Scalars['ID']>;
+  collectionSlug?: Maybe<Scalars['String']>;
   groupByProduct?: Maybe<Scalars['Boolean']>;
   take?: Maybe<Scalars['Int']>;
   skip?: Maybe<Scalars['Int']>;

+ 35 - 2
packages/core/e2e/default-search-plugin.e2e-spec.ts

@@ -189,6 +189,23 @@ describe('Default search plugin', () => {
         ]);
     }
 
+    async function testMatchCollectionSlug(client: SimpleGraphQLClient) {
+        const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
+            SEARCH_PRODUCTS_SHOP,
+            {
+                input: {
+                    collectionSlug: 'plants',
+                    groupByProduct: true,
+                },
+            },
+        );
+        expect(result.search.items.map(i => i.productName)).toEqual([
+            'Spiky Cactus',
+            'Orchid',
+            'Bonsai Tree',
+        ]);
+    }
+
     async function testSinglePrices(client: SimpleGraphQLClient) {
         const result = await client.query<SearchGetPrices.Query, SearchGetPrices.Variables>(
             SEARCH_GET_PRICES,
@@ -254,6 +271,8 @@ describe('Default search plugin', () => {
 
         it('matches by collectionId', () => testMatchCollectionId(shopClient));
 
+        it('matches by collectionSlug', () => testMatchCollectionSlug(shopClient));
+
         it('single prices', () => testSinglePrices(shopClient));
 
         it('price ranges', () => testPriceRanges(shopClient));
@@ -406,6 +425,8 @@ describe('Default search plugin', () => {
 
         it('matches by collectionId', () => testMatchCollectionId(adminClient));
 
+        it('matches by collectionSlug', () => testMatchCollectionSlug(adminClient));
+
         it('single prices', () => testSinglePrices(adminClient));
 
         it('price ranges', () => testPriceRanges(adminClient));
@@ -531,9 +552,21 @@ describe('Default search plugin', () => {
                 await awaitRunningJobs(adminClient);
                 // add an additional check for the collection filters to update
                 await awaitRunningJobs(adminClient);
-                const result = await doAdminSearchQuery({ collectionId: 'T_2', groupByProduct: true });
+                const result1 = await doAdminSearchQuery({ collectionId: 'T_2', groupByProduct: true });
 
-                expect(result.search.items.map(i => i.productName)).toEqual([
+                expect(result1.search.items.map(i => i.productName)).toEqual([
+                    'Road Bike',
+                    'Skipping Rope',
+                    'Boxing Gloves',
+                    'Tent',
+                    'Cruiser Skateboard',
+                    'Football',
+                    'Running Shoe',
+                ]);
+
+                const result2 = await doAdminSearchQuery({ collectionSlug: 'plants', groupByProduct: true });
+
+                expect(result2.search.items.map(i => i.productName)).toEqual([
                     'Road Bike',
                     'Skipping Rope',
                     'Boxing Gloves',

+ 42 - 41
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -3170,6 +3170,7 @@ export type SearchInput = {
     facetValueIds?: Maybe<Array<Scalars['ID']>>;
     facetValueOperator?: Maybe<LogicalOperator>;
     collectionId?: Maybe<Scalars['ID']>;
+    collectionSlug?: Maybe<Scalars['String']>;
     groupByProduct?: Maybe<Scalars['Boolean']>;
     take?: Maybe<Scalars['Int']>;
     skip?: Maybe<Scalars['Int']>;
@@ -4019,20 +4020,6 @@ export type CreateCountryMutation = { __typename?: 'Mutation' } & {
     createCountry: { __typename?: 'Country' } & CountryFragment;
 };
 
-export type CustomerGroupFragment = { __typename?: 'CustomerGroup' } & Pick<CustomerGroup, 'id' | 'name'> & {
-        customers: { __typename?: 'CustomerList' } & Pick<CustomerList, 'totalItems'> & {
-                items: Array<{ __typename?: 'Customer' } & Pick<Customer, 'id'>>;
-            };
-    };
-
-export type CreateCustomerGroupMutationVariables = {
-    input: CreateCustomerGroupInput;
-};
-
-export type CreateCustomerGroupMutation = { __typename?: 'Mutation' } & {
-    createCustomerGroup: { __typename?: 'CustomerGroup' } & CustomerGroupFragment;
-};
-
 export type UpdateCustomerGroupMutationVariables = {
     input: UpdateCustomerGroupInput;
 };
@@ -4083,15 +4070,6 @@ export type AddCustomersToGroupMutation = { __typename?: 'Mutation' } & {
     addCustomersToGroup: { __typename?: 'CustomerGroup' } & CustomerGroupFragment;
 };
 
-export type RemoveCustomersFromGroupMutationVariables = {
-    groupId: Scalars['ID'];
-    customerIds: Array<Scalars['ID']>;
-};
-
-export type RemoveCustomersFromGroupMutation = { __typename?: 'Mutation' } & {
-    removeCustomersFromGroup: { __typename?: 'CustomerGroup' } & CustomerGroupFragment;
-};
-
 export type GetCustomerWithGroupsQueryVariables = {
     id: Scalars['ID'];
 };
@@ -5078,6 +5056,29 @@ export type GetOrderQuery = { __typename?: 'Query' } & {
     order?: Maybe<{ __typename?: 'Order' } & OrderWithLinesFragment>;
 };
 
+export type CustomerGroupFragment = { __typename?: 'CustomerGroup' } & Pick<CustomerGroup, 'id' | 'name'> & {
+        customers: { __typename?: 'CustomerList' } & Pick<CustomerList, 'totalItems'> & {
+                items: Array<{ __typename?: 'Customer' } & Pick<Customer, 'id'>>;
+            };
+    };
+
+export type CreateCustomerGroupMutationVariables = {
+    input: CreateCustomerGroupInput;
+};
+
+export type CreateCustomerGroupMutation = { __typename?: 'Mutation' } & {
+    createCustomerGroup: { __typename?: 'CustomerGroup' } & CustomerGroupFragment;
+};
+
+export type RemoveCustomersFromGroupMutationVariables = {
+    groupId: Scalars['ID'];
+    customerIds: Array<Scalars['ID']>;
+};
+
+export type RemoveCustomersFromGroupMutation = { __typename?: 'Mutation' } & {
+    removeCustomersFromGroup: { __typename?: 'CustomerGroup' } & CustomerGroupFragment;
+};
+
 export type UpdateOptionGroupMutationVariables = {
     input: UpdateProductOptionGroupInput;
 };
@@ -6082,18 +6083,6 @@ export namespace CreateCountry {
     export type CreateCountry = CountryFragment;
 }
 
-export namespace CustomerGroup {
-    export type Fragment = CustomerGroupFragment;
-    export type Customers = CustomerGroupFragment['customers'];
-    export type Items = NonNullable<CustomerGroupFragment['customers']['items'][0]>;
-}
-
-export namespace CreateCustomerGroup {
-    export type Variables = CreateCustomerGroupMutationVariables;
-    export type Mutation = CreateCustomerGroupMutation;
-    export type CreateCustomerGroup = CustomerGroupFragment;
-}
-
 export namespace UpdateCustomerGroup {
     export type Variables = UpdateCustomerGroupMutationVariables;
     export type Mutation = UpdateCustomerGroupMutation;
@@ -6129,12 +6118,6 @@ export namespace AddCustomersToGroup {
     export type AddCustomersToGroup = CustomerGroupFragment;
 }
 
-export namespace RemoveCustomersFromGroup {
-    export type Variables = RemoveCustomersFromGroupMutationVariables;
-    export type Mutation = RemoveCustomersFromGroupMutation;
-    export type RemoveCustomersFromGroup = CustomerGroupFragment;
-}
-
 export namespace GetCustomerWithGroups {
     export type Variables = GetCustomerWithGroupsQueryVariables;
     export type Query = GetCustomerWithGroupsQuery;
@@ -6767,6 +6750,24 @@ export namespace GetOrder {
     export type Order = OrderWithLinesFragment;
 }
 
+export namespace CustomerGroup {
+    export type Fragment = CustomerGroupFragment;
+    export type Customers = CustomerGroupFragment['customers'];
+    export type Items = NonNullable<CustomerGroupFragment['customers']['items'][0]>;
+}
+
+export namespace CreateCustomerGroup {
+    export type Variables = CreateCustomerGroupMutationVariables;
+    export type Mutation = CreateCustomerGroupMutation;
+    export type CreateCustomerGroup = CustomerGroupFragment;
+}
+
+export namespace RemoveCustomersFromGroup {
+    export type Variables = RemoveCustomersFromGroupMutationVariables;
+    export type Mutation = RemoveCustomersFromGroupMutation;
+    export type RemoveCustomersFromGroup = CustomerGroupFragment;
+}
+
 export namespace UpdateOptionGroup {
     export type Variables = UpdateOptionGroupMutationVariables;
     export type Mutation = UpdateOptionGroupMutation;

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

@@ -2146,6 +2146,7 @@ export type SearchInput = {
     facetValueIds?: Maybe<Array<Scalars['ID']>>;
     facetValueOperator?: Maybe<LogicalOperator>;
     collectionId?: Maybe<Scalars['ID']>;
+    collectionSlug?: Maybe<Scalars['String']>;
     groupByProduct?: Maybe<Scalars['Boolean']>;
     take?: Maybe<Scalars['Int']>;
     skip?: Maybe<Scalars['Int']>;

+ 1 - 0
packages/core/src/api/schema/common/common-types.graphql

@@ -126,6 +126,7 @@ input SearchInput {
     facetValueIds: [ID!]
     facetValueOperator: LogicalOperator
     collectionId: ID
+    collectionSlug: String
     groupByProduct: Boolean
     take: Int
     skip: Int

+ 12 - 11
packages/core/src/plugin/default-search-plugin/indexer/indexer.controller.ts

@@ -59,7 +59,7 @@ export class IndexerController {
     @MessagePattern(ReindexMessage.pattern)
     reindex({ ctx: rawContext }: ReindexMessage['data']): Observable<ReindexMessage['response']> {
         const ctx = RequestContext.deserialize(rawContext);
-        return asyncObservable(async (observer) => {
+        return asyncObservable(async observer => {
             const timeStart = Date.now();
             const qb = this.getSearchIndexQueryBuilder(ctx.channelId);
             const count = await qb.getCount();
@@ -103,7 +103,7 @@ export class IndexerController {
     }: UpdateVariantsByIdMessage['data']): Observable<UpdateVariantsByIdMessage['response']> {
         const ctx = RequestContext.deserialize(rawContext);
 
-        return asyncObservable(async (observer) => {
+        return asyncObservable(async observer => {
             const timeStart = Date.now();
             if (ids.length) {
                 const batches = Math.ceil(ids.length / BATCH_SIZE);
@@ -169,7 +169,7 @@ export class IndexerController {
                 await this.removeSearchIndexItems(
                     ctx.languageCode,
                     ctx.channelId,
-                    variants.map((v) => v.id),
+                    variants.map(v => v.id),
                 );
             }
             return true;
@@ -238,14 +238,14 @@ export class IndexerController {
         });
         if (product) {
             let updatedVariants = await this.connection.getRepository(ProductVariant).findByIds(
-                product.variants.map((v) => v.id),
+                product.variants.map(v => v.id),
                 {
                     relations: variantRelations,
                     where: { deletedAt: null },
                 },
             );
             if (product.enabled === false) {
-                updatedVariants.forEach((v) => (v.enabled = false));
+                updatedVariants.forEach(v => (v.enabled = false));
             }
             Logger.verbose(`Updating ${updatedVariants.length} variants`, workerLoggerCtx);
             updatedVariants = this.hydrateVariants(ctx, updatedVariants);
@@ -282,7 +282,7 @@ export class IndexerController {
             relations: ['variants'],
         });
         if (product) {
-            const removedVariantIds = product.variants.map((v) => v.id);
+            const removedVariantIds = product.variants.map(v => v.id);
             if (removedVariantIds.length) {
                 await this.removeSearchIndexItems(ctx.languageCode, channelId, removedVariantIds);
             }
@@ -309,8 +309,8 @@ export class IndexerController {
      */
     private hydrateVariants(ctx: RequestContext, variants: ProductVariant[]): ProductVariant[] {
         return variants
-            .map((v) => this.productVariantService.applyChannelPriceAndTax(v, ctx))
-            .map((v) => translateDeep(v, ctx.languageCode, ['product']));
+            .map(v => this.productVariantService.applyChannelPriceAndTax(v, ctx))
+            .map(v => translateDeep(v, ctx.languageCode, ['product', 'collections']));
     }
 
     private async saveVariants(languageCode: LanguageCode, channelId: ID, variants: ProductVariant[]) {
@@ -337,10 +337,11 @@ export class IndexerController {
                     productVariantAssetId: v.featuredAsset ? v.featuredAsset.id : null,
                     productPreview: v.product.featuredAsset ? v.product.featuredAsset.preview : '',
                     productVariantPreview: v.featuredAsset ? v.featuredAsset.preview : '',
-                    channelIds: v.product.channels.map((c) => c.id as string),
+                    channelIds: v.product.channels.map(c => c.id as string),
                     facetIds: this.getFacetIds(v),
                     facetValueIds: this.getFacetValueIds(v),
-                    collectionIds: v.collections.map((c) => c.id.toString()),
+                    collectionIds: v.collections.map(c => c.id.toString()),
+                    collectionSlugs: v.collections.map(c => c.slug),
                 }),
         );
         await this.queue.push(() => this.connection.getRepository(SearchIndexItem).save(items));
@@ -364,7 +365,7 @@ export class IndexerController {
      * Remove items from the search index
      */
     private async removeSearchIndexItems(languageCode: LanguageCode, channelId: ID, variantIds: ID[]) {
-        const compositeKeys = variantIds.map((id) => ({
+        const compositeKeys = variantIds.map(id => ({
             productVariantId: id,
             channelId,
             languageCode,

+ 3 - 0
packages/core/src/plugin/default-search-plugin/search-index-item.entity.ts

@@ -62,6 +62,9 @@ export class SearchIndexItem {
     @Column('simple-array')
     collectionIds: string[];
 
+    @Column('simple-array')
+    collectionSlugs: string[];
+
     @Column('simple-array')
     channelIds: string[];
 

+ 4 - 1
packages/core/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts

@@ -108,7 +108,7 @@ export class MysqlSearchStrategy implements SearchStrategy {
         qb: SelectQueryBuilder<SearchIndexItem>,
         input: SearchInput,
     ): SelectQueryBuilder<SearchIndexItem> {
-        const { term, facetValueIds, facetValueOperator, collectionId } = input;
+        const { term, facetValueIds, facetValueOperator, collectionId, collectionSlug } = input;
 
         qb.where('1 = 1');
         if (term && term.length > this.minTermLength) {
@@ -150,6 +150,9 @@ export class MysqlSearchStrategy implements SearchStrategy {
         if (collectionId) {
             qb.andWhere(`FIND_IN_SET (:collectionId, collectionIds)`, { collectionId });
         }
+        if (collectionSlug) {
+            qb.andWhere(`FIND_IN_SET (:collectionSlug, collectionSlugs)`, { collectionSlug });
+        }
         qb.andWhere('languageCode = :languageCode', { languageCode: ctx.languageCode });
         qb.andWhere('channelId = :channelId', { channelId: ctx.channelId });
         if (input.groupByProduct === true) {

+ 6 - 1
packages/core/src/plugin/default-search-plugin/search-strategy/postgres-search-strategy.ts

@@ -110,7 +110,7 @@ export class PostgresSearchStrategy implements SearchStrategy {
         input: SearchInput,
         forceGroup: boolean = false,
     ): SelectQueryBuilder<SearchIndexItem> {
-        const { term, facetValueIds, facetValueOperator, collectionId } = input;
+        const { term, facetValueIds, facetValueOperator, collectionId, collectionSlug } = input;
         // join multiple words with the logical AND operator
         const termLogicalAnd = term ? term.trim().replace(/\s+/, ' & ') : '';
 
@@ -158,6 +158,11 @@ export class PostgresSearchStrategy implements SearchStrategy {
         if (collectionId) {
             qb.andWhere(`:collectionId = ANY (string_to_array(si.collectionIds, ','))`, { collectionId });
         }
+        if (collectionSlug) {
+            qb.andWhere(`:collectionSlug = ANY (string_to_array(si.collectionSlugs, ','))`, {
+                collectionSlug,
+            });
+        }
         qb.andWhere('si.languageCode = :languageCode', { languageCode: ctx.languageCode });
         qb.andWhere('si.channelId = :channelId', { channelId: ctx.channelId });
         if (input.groupByProduct === true) {

+ 6 - 1
packages/core/src/plugin/default-search-plugin/search-strategy/sqlite-search-strategy.ts

@@ -105,7 +105,7 @@ export class SqliteSearchStrategy implements SearchStrategy {
         qb: SelectQueryBuilder<SearchIndexItem>,
         input: SearchInput,
     ): SelectQueryBuilder<SearchIndexItem> {
-        const { term, facetValueIds, facetValueOperator, collectionId } = input;
+        const { term, facetValueIds, facetValueOperator, collectionId, collectionSlug } = input;
 
         qb.where('1 = 1');
         if (term && term.length > this.minTermLength) {
@@ -150,6 +150,11 @@ export class SqliteSearchStrategy implements SearchStrategy {
                 collectionId: `%,${collectionId},%`,
             });
         }
+        if (collectionSlug) {
+            qb.andWhere(`(',' || collectionSlugs || ',') LIKE :collectionSlug`, {
+                collectionSlug: `%,${collectionSlug},%`,
+            });
+        }
         qb.andWhere('languageCode = :languageCode', { languageCode: ctx.languageCode });
         qb.andWhere('channelId = :channelId', { channelId: ctx.channelId });
         if (input.groupByProduct === true) {

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

@@ -3170,6 +3170,7 @@ export type SearchInput = {
     facetValueIds?: Maybe<Array<Scalars['ID']>>;
     facetValueOperator?: Maybe<LogicalOperator>;
     collectionId?: Maybe<Scalars['ID']>;
+    collectionSlug?: Maybe<Scalars['String']>;
     groupByProduct?: Maybe<Scalars['Boolean']>;
     take?: Maybe<Scalars['Int']>;
     skip?: Maybe<Scalars['Int']>;

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
schema-admin.json


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
schema-shop.json


Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor