From 4923748a5a02a5e42b8a05a1dfb830a3a6c37bdf Mon Sep 17 00:00:00 2001 From: ryrahul Date: Sat, 9 May 2026 14:42:24 +0530 Subject: [PATCH] fix: assign facets to channel when re-importing products via CSV Fixes #4673 --- packages/core/e2e/import.e2e-spec.ts | 65 ++++++++++++++++++- .../providers/importer/importer.ts | 17 +++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/packages/core/e2e/import.e2e-spec.ts b/packages/core/e2e/import.e2e-spec.ts index 3132916fdc..7249d4c2b3 100644 --- a/packages/core/e2e/import.e2e-spec.ts +++ b/packages/core/e2e/import.e2e-spec.ts @@ -1,8 +1,9 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { omit } from '@vendure/common/lib/omit'; +import { CurrencyCode, LanguageCode } from '@vendure/common/lib/generated-types'; import { User } from '@vendure/core'; -import { createTestEnvironment } from '@vendure/testing'; +import { createTestEnvironment, E2E_DEFAULT_CHANNEL_TOKEN } from '@vendure/testing'; import * as fs from 'node:fs'; import http from 'node:http'; import path from 'node:path'; @@ -12,6 +13,7 @@ import { initialData } from '../../../e2e-common/e2e-initial-data'; import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config'; import { graphql } from './graphql/graphql-admin'; +import { createChannelDocument } from './graphql/shared-definitions'; describe('Import resolver', () => { const { server, adminClient } = createTestEnvironment({ @@ -297,6 +299,67 @@ describe('Import resolver', () => { expect(tShirt.variants[0].options.length).toBe(2); }, 30000); + // https://github.com/vendure-ecommerce/vendure/issues/4673 + it('imports facets and variantFacets when re-importing into a new channel', async () => { + const SECOND_CHANNEL_TOKEN = 'second_channel_token'; + + // Create a new channel + await adminClient.query(createChannelDocument, { + input: { + code: 'second-channel', + token: SECOND_CHANNEL_TOKEN, + defaultLanguageCode: LanguageCode.en, + currencyCode: CurrencyCode.USD, + pricesIncludeTax: false, + defaultShippingZoneId: 'T_1', + defaultTaxZoneId: 'T_1', + }, + }); + + // Switch to the new channel + adminClient.setChannelToken(SECOND_CHANNEL_TOKEN); + + // Import the same CSV into the new channel + const csvFile = path.join(__dirname, 'fixtures', 'product-import.csv'); + const result = await adminClient.fileUploadMutation({ + mutation: importProductsDocument1, + filePaths: [csvFile], + mapVariables: () => ({ csvFile: null }), + }); + + expect(result.importProducts.errors).toEqual([ + 'Invalid Record Length: header length is 20, got 1 on line 8', + ]); + expect(result.importProducts.imported).toBe(4); + + // Query products in the new channel + const productResult = await adminClient.query(getProductsDocument1, { + options: {}, + }); + + expect(productResult.products.totalItems).toBe(4); + + const paperStretcher = productResult.products.items.find( + (p: any) => p.name === 'Perfect Paper Stretcher', + ); + const smock = productResult.products.items.find((p: any) => p.name === 'Artists Smock'); + + if (!paperStretcher || !smock) { + throw new Error('Expected products to be found in second channel'); + } + + const byName = (e: { name: string }) => e.name; + + // Verify product-level facets are present in the new channel + expect(smock.facetValues.map(byName).sort()).toEqual(['Denim', 'clothes']); + + // Verify variant-level facets are present in the new channel + expect(paperStretcher.variants[0].facetValues.map(byName).sort()).toEqual(['Accessory', 'KB']); + + // Switch back to default channel + adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN); + }, 30000); + describe('asset urls', () => { let staticServer: http.Server; diff --git a/packages/core/src/data-import/providers/importer/importer.ts b/packages/core/src/data-import/providers/importer/importer.ts index 9c588e2e61..d594335974 100644 --- a/packages/core/src/data-import/providers/importer/importer.ts +++ b/packages/core/src/data-import/providers/importer/importer.ts @@ -161,6 +161,11 @@ export class Importer { rows: ParsedProductWithVariants[], onProgress: OnProgressFn, ): Promise { + // Clear caches to avoid stale references from previous imports + // (e.g. when importing into a different channel) + this.facetMap.clear(); + this.facetValueMap.clear(); + this.taxCategoryMatches = {}; let errors: string[] = []; let imported = 0; const languageCode = ctx.languageCode; @@ -357,6 +362,12 @@ export class Importer { ); if (existing) { facetEntity = existing; + await this.channelService.assignToChannels( + ctx, + Facet, + facetEntity.id, + [ctx.channelId], + ); } else { facetEntity = await this.facetService.create(ctx, { isPrivate: false, @@ -381,6 +392,12 @@ export class Importer { const existing = facetEntity.values.find(v => v.name === valueName); if (existing) { facetValueEntity = existing; + await this.channelService.assignToChannels( + ctx, + FacetValue, + facetValueEntity.id, + [ctx.channelId], + ); } else { facetValueEntity = await this.facetValueService.create(ctx, facetEntity, { code: normalizeString(valueName, '-'),