Skip to content

feat(core): Support shared product option groups in CSV import #4482

@michaelbromley

Description

@michaelbromley

Summary

The CSV product importer (Importer.importProducts()) currently creates a separate option group per product, even when multiple products have identical option groups with identical options. With the new shared/channel-aware option group model (#4469), the importer should consolidate identical option groups into a single shared group.

Current behaviour

For each product row in the CSV, the importer:

  1. Creates a new ProductOptionGroup with code {product-name}-{group-name} (e.g. ultraboost-running-shoe-size)
  2. Creates new ProductOption entities for each value
  3. Associates the group with the product

This means 6 shoes with identical "size" options (Size 40, 42, 44, 46) produce 6 separate option groups, each with 4 duplicate options — 24 option entities instead of 4.

Proposed behaviour

Auto-sharing heuristic (default)

Before the main product import loop, do a first pass over all parsed products:

  1. Build a signature map keyed by groupName + sorted option values (across all translations)
  2. Groups that appear in 2+ products with identical signatures are created once and shared
  3. Groups unique to one product are created as before

Option group code generation

Scenario Code
Shared group (identical options across products) {group-name} e.g. size
Unique group (only one product) {product-name}-{group-name} e.g. curvy-monitor-monitor-size (unchanged)
Code collision on shared group name Append numeric suffix: size, size-2

Display names and option names are unaffected — they come directly from the CSV values.

Explicit code via CSV format extension

Extend the optionGroups column to support an optional :code suffix:

optionGroups
"size:shoe-size|color:shoe-color"

Where size is the display name and shoe-size is the explicit code. When two products specify the same explicit code, they share the group deterministically. If no :code part is given, fall back to the auto-sharing heuristic.

This gives importers explicit control over sharing and disambiguation (e.g. shoe "size" vs RAM "size" could use size:shoe-size and size:memory-capacity).

Opt-out flag

Add a shareOptionGroups?: boolean option (default true) to Importer.importProducts():

async importProducts(
    ctx: RequestContext,
    rows: ParsedProductWithVariants[],
    onProgress: OnProgressFn,
    options?: { shareOptionGroups?: boolean },
): Promise<string[]>

This preserves backward compatibility for anyone relying on the current per-product behaviour. The parseAndImport() method and GraphQL mutation would use the default (true).

Backward compatibility

  • Importer, FastImporterService, and ImportParser are all publicly exported from @vendure/core
  • importProducts() is called directly by load-testing benchmarks and potentially by user plugins/scripts
  • The importProductsFromCsv() CLI helper is used by @vendure/testing and @vendure/create
  • Option group codes will change for shared groups (e.g. ultraboost-running-shoe-sizesize), affecting anyone querying by code after re-populate
  • The ParsedOptionGroup interface would get a new optional code?: string field (non-breaking)

Since this targets the minor branch, the behaviour change with opt-out is acceptable.

Demo data consolidation

The packages/create/assets/products.csv should be updated to demonstrate sharing. Current duplicates:

Group name Options Products (currently 6 separate groups)
size Size 40, Size 42, Size 44, Size 46 Ultraboost, Freerun, Hi-Top Basketball, Pureboost, RunX, Allstar

The RAM product also uses "size" but with different values (4GB, 8GB, 16GB) — the heuristic correctly keeps this separate.

Implementation plan

  1. Add optional code?: string to ParsedOptionGroup interface
  2. Update ImportParser.parseProductFromRecord() to parse name:code syntax
  3. Add first-pass signature analysis in importProducts() before the product loop
  4. Pre-create shared option groups and build a shared optionsMap
  5. In the product loop, look up shared groups before creating new ones
  6. Update products.csv demo data
  7. Update E2E tests in packages/core/e2e/import.e2e-spec.ts

Relates to #4469, #4478

Metadata

Metadata

Labels

No labels
No labels

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions