parse-shopify-csv
Version:
Parse shopify product csv
1,210 lines (916 loc) • 54.6 kB
Markdown
[](https://www.npmjs.com/package/parse-shopify-csv)
[](https://opensource.org/licenses/MIT)
# Shopify CSV Parser (`parse-shopify-csv`)
A robust, type-safe, and production-ready Node.js library for intelligently parsing, modifying, and writing complex Shopify product CSV files, with first-class support for metafields.
### The Problem
Shopify's product CSV format is powerful but complex. A single logical product can span multiple rows to define its variants, images, and other attributes. A simple CSV parser sees these as disconnected rows, making it notoriously difficult to reconstruct the product hierarchy correctly.
### The Solution
This library is purpose-built to understand the Shopify CSV structure. It handles all the complexity for you, parsing the entire file into a structured, hierarchical JavaScript object. It intelligently groups all variants, images, and metafields under their parent product, allowing you to work with your data in a simple and intuitive way.
## Key Features
- **Intelligent Hierarchy Parsing:** Correctly aggregates multiple CSV rows into single, complete product objects.
- **Flexible Schema Support:** Automatically adapts to various CSV export formats including market-specific pricing (US, International, etc.), varying Google Shopping fields, and custom columns. See [Flexible Schemas Guide](docs/FLEXIBLE-SCHEMAS.md).
- **Market-Specific Pricing:** Built-in support for international pricing fields like `Price / United States`, `Price / International` with dedicated utilities for analysis and manipulation.
- **Seamless Metafield Manipulation:** Automatically parses metafield columns into a dedicated, iterable `metadata` object. Changes to this object are **automatically synced** back to the raw data for effortless writing.
- **Powerful Utility Functions:** A rich set of helpers to perform CRUD (Create, Read, Update, Delete) and query operations on products, variants, images, and metafields.
- **Iterable by Default:** The returned product collection and each product's metafields are directly iterable. Use them in `for...of` loops, with the spread syntax (`...`), and more.
- **Fully Type-Safe with Generics:** Use TypeScript generics to provide strong types for your custom metafield columns, enabling full auto-completion and compile-time safety.
- **Complete Read-Modify-Write Workflow:** Provides a full toolkit (`parse`, `stringify`, `write`) to programmatically read, modify, and save Shopify CSVs.
- **Robust Error Handling:** Throws a custom `CSVProcessingError` for predictable handling of file I/O issues, malformed CSVs, and missing required columns.
## Installation
```bash
npm install parse-shopify-csv
```
## Quick Start: The Read-Modify-Write Workflow
The most common use case is to read a Shopify export, perform bulk edits, and save the result. This library makes that simple.
Here's a complete example that adds a `new-collection` tag to every product using the modern tag utilities.
```typescript
import { parseShopifyCSV, writeShopifyCSV, addTag, CSVProcessingError } from 'parse-shopify-csv';
async function bulkUpdateTags(inputFile: string, outputFile: string) {
try {
// 1. Read and parse the CSV
const products = await parseShopifyCSV(inputFile);
// 2. Modify the data using tag utilities
// The result is iterable, so we can use a for...of loop
for (const product of products) {
console.log(`Updating tags for: ${product.data.Title}`);
// Add tag with automatic deduplication
addTag(product, 'new-collection');
}
// 3. Write the modified data back to a new file
await writeShopifyCSV(outputFile, products);
console.log(`✅ Successfully updated products and saved to ${outputFile}`);
} catch (error) {
if (error instanceof CSVProcessingError) {
console.error(`A CSV processing error occurred: ${error.message}`);
} else {
console.error("An unexpected error occurred:", error);
}
}
}
bulkUpdateTags('shopify-export.csv', 'shopify-export-modified.csv');
```
## Parsing Complex CSVs with Type Safety
The library automatically handles complex Shopify exports with metafields, Google Shopping fields, and market-specific pricing. Here's a practical example:
```typescript
import {
parseShopifyCSVFromString,
extractMarketPricing,
getGoogleShoppingAttributes,
} from 'parse-shopify-csv';
async function parseComplexJewelryCSV() {
// Your complex CSV with metafields and Google Shopping fields
const csvData = `Handle,Title,Vendor,Type,Google Shopping / Gender,Google Shopping / Age Group,Metal (product.metafields.product.metal),Chain Length (product.metafields.product.chain_length),Occasions (product.metafields.product.occasions),Price / United States,Price / International,Status
heart-necklace,Sterling Silver Heart Necklace,Premium Jewelry,Necklace,unisex,adult,Sterling Silver,18 inches,Valentine's Day,89.99,99.99,active`;
// Parse the CSV - automatically detects all field types
const products = await parseShopifyCSVFromString(csvData);
const product = products['heart-necklace'];
// Access basic product data
console.log(`Product: ${product.data.Title}`);
console.log(`Vendor: ${product.data.Vendor}`);
console.log(`Type: ${product.data.Type}`);
// Access Google Shopping fields (structured approach)
const googleShopping = getGoogleShoppingAttributes(product);
console.log(`Google Gender: ${googleShopping.gender}`);
console.log(`Google Age Group: ${googleShopping.ageGroup}`);
console.log(`Google Category: ${googleShopping.category}`);
console.log(`Google Condition: ${googleShopping.condition}`);
// Alternative: raw column access
console.log(`Google Gender (raw): ${product.data['Google Shopping / Gender']}`);
// Access metafields via parsed metadata object (recommended)
console.log(`Metal: ${product.metadata['product.metal']?.value}`);
console.log(`Chain Length: ${product.metadata['product.chain_length']?.value}`);
console.log(`Occasions: ${product.metadata['product.occasions']?.value}`);
// Alternative: access metafields via raw column names
console.log(`Metal (raw): ${product.data['Metal (product.metafields.product.metal)']}`);
// Work with market-specific pricing
const marketPrices = extractMarketPricing(product.data);
console.log('Market Pricing:', marketPrices);
// Output: { 'United States': { price: '89.99' }, 'International': { price: '99.99' } }
// Type-safe filtering using metafields
const silverJewelry = Object.values(products).filter(p =>
p.metadata['product.metal']?.value?.toLowerCase().includes('silver')
);
const valentinesItems = Object.values(products).filter(p =>
p.metadata['product.occasions']?.value?.toLowerCase().includes('valentine')
);
// Filter using Google Shopping attributes
const unisexItems = Object.values(products).filter(p => {
const googleShopping = getGoogleShoppingAttributes(p);
return googleShopping.gender === 'unisex';
});
const adultItems = Object.values(products).filter(p => {
const googleShopping = getGoogleShoppingAttributes(p);
return googleShopping.ageGroup === 'adult';
});
console.log(`Found ${silverJewelry.length} silver items, ${valentinesItems.length} Valentine's items`);
console.log(`Found ${unisexItems.length} unisex items, ${adultItems.length} adult items`);
}
```
### Advanced Schema Analysis (Optional)
For advanced use cases, you can analyze your CSV structure before parsing:
```typescript
import { detectCSVSchema, generateTypeScriptInterface } from 'parse-shopify-csv';
// Analyze your CSV structure
const headers = csvData.split('\n')[0].split(',');
const schema = detectCSVSchema(headers, {
detectMarketPricing: true,
detectGoogleShopping: true,
detectVariantFields: true
});
console.log(`Detected: ${schema.metafieldColumns.length} metafields, ${schema.marketPricingFields.length} market prices`);
// Generate TypeScript interface for your codebase
const tsInterface = generateTypeScriptInterface(headers, 'JewelryCSVSchema');
// Copy this interface to your TypeScript files for full type safety
```
### Two Ways to Access Metafields
The library provides two convenient ways to access metafield data:
1. **Structured access** (recommended): `product.metadata['namespace.key']?.value`
- Clean, consistent format
- Automatically parsed with proper typing
- Easy to work with programmatically
2. **Raw column access**: `product.data['Display Name (product.metafields.namespace.key)']`
- Direct access to original column values
- Useful when you need the exact CSV column name
- Good for debugging or one-off access
### Google Shopping Field Access
For Google Shopping fields, use the structured utility functions:
```typescript
import {
getGoogleShoppingAttributes,
setGoogleShoppingAttributes,
bulkSetGoogleShoppingAttributes
} from 'parse-shopify-csv';
// Get Google Shopping attributes (fully type-safe)
const googleShopping = getGoogleShoppingAttributes(product);
console.log(googleShopping.gender); // 'unisex' | 'male' | 'female'
console.log(googleShopping.ageGroup); // 'adult' | 'newborn' | 'infant' | 'toddler' | 'kids'
console.log(googleShopping.condition); // 'new' | 'refurbished' | 'used'
console.log(googleShopping.sizeSystem); // 'US' | 'EU' | 'UK' | 'AU' | etc.
// Set Google Shopping attributes on a single product (type-safe)
setGoogleShoppingAttributes(product, {
gender: 'unisex', // TypeScript will suggest: 'male' | 'female' | 'unisex'
condition: 'new', // TypeScript will suggest: 'new' | 'refurbished' | 'used'
ageGroup: 'adult', // TypeScript will suggest: 'adult' | 'newborn' | etc.
sizeSystem: 'US', // TypeScript will suggest: 'US' | 'EU' | 'UK' | etc.
customLabel0: 'premium',
category: 'Apparel & Accessories > Jewelry'
});
// Bulk set Google Shopping attributes on multiple products
const allProducts = Object.values(products);
bulkSetGoogleShoppingAttributes(allProducts, {
condition: 'new',
customLabel0: 'premium-collection'
});
```
This provides clean, consistent access to all Google Shopping attributes with **full type safety** - TypeScript will autocomplete valid values and catch invalid ones at compile time.
This approach provides:
- **Automatic parsing** of all field types without configuration
- **Structured metafield access** via the `metadata` object
- **Type-safe Google Shopping access** via utility functions with enum validation
- **Market pricing utilities** for international stores
- **Flexible data access** via both structured and raw column names
- **Bulk operations** for efficient mass updates
- **Full TypeScript IntelliSense** for all Google Shopping field values
## Utility Functions for Data Manipulation
Beyond parsing, this library includes a comprehensive set of utility functions to simplify common data manipulation tasks.
### Tag Management
The library provides modern, powerful tag management utilities that handle all the complexity of working with Shopify's comma-separated tag format. All tag operations are case-insensitive and handle deduplication automatically.
```typescript
import {
parseShopifyCSV,
addTag,
removeTags,
hasTag,
getTags,
findProductsByTag,
getAllTags,
getTagStats
} from 'parse-shopify-csv';
async function manageTags() {
const products = await parseShopifyCSV('shopify-export.csv');
// Get the first product for demonstration
const product = Object.values(products)[0];
// Basic tag operations
console.log('Current tags:', getTags(product));
addTag(product, 'featured'); // Add single tag
addTags(product, ['sale', 'bestseller']); // Add multiple tags
removeTag(product, 'old-tag'); // Remove single tag
removeTags(product, ['clearance', 'discontinued']); // Remove multiple
// Tag checking
if (hasTag(product, 'sale')) {
addTag(product, 'promotional');
}
if (hasAllTags(product, ['summer', 'cotton'])) {
addTag(product, 'summer-cotton-collection');
}
// Finding products by tags
const saleProducts = findProductsByTag(products, 'sale');
const summerCottonProducts = findProductsByTags(products, ['summer', 'cotton']);
// Tag analytics
const allTags = getAllTags(products);
const tagUsage = getTagStats(products);
console.log('All tags in store:', allTags);
console.log('Most popular tags:', Object.entries(tagUsage)
.sort(([,a], [,b]) => b - a)
.slice(0, 5));
}
```
#### Inventory Management
```typescript
import {
parseShopifyCSV,
updateInventoryQuantity,
bulkUpdateInventory,
findVariant
} from 'parse-shopify-csv';
async function manageInventory() {
const products = await parseShopifyCSV('shopify-export.csv');
const product = Object.values(products)[0];
// Update single variant inventory
updateInventoryQuantity(product, 'SKU-123', 50);
// Bulk update multiple SKUs
const inventoryUpdates = {
'SKU-123': 25,
'SKU-456': 100,
'SKU-789': 0
};
const updatedProducts = bulkUpdateInventory(products, inventoryUpdates);
console.log(`Updated inventory for ${updatedProducts.length} products`);
}
```
#### Advanced Variant & Image Management
```typescript
import {
parseShopifyCSV,
writeShopifyCSV,
bulkUpdateVariantField,
findDuplicateImages,
assignBulkImagesToVariants,
findVariant,
addVariant,
removeVariant,
findVariantByOptions,
findAllVariants,
addImage,
assignImageToVariant,
findImagesWithoutAltText,
findOrphanedImages,
toVariantArray,
toImageArray,
ImageAssignmentRule
} from 'parse-shopify-csv';
async function manageVariantsAndImages() {
const products = await parseShopifyCSV('shopify-export.csv');
// 1. Bulk update variant prices with a 10% increase
const modifiedProducts = bulkUpdateVariantField(products, 'Variant Price',
(variant, product) => {
const currentPrice = parseFloat(variant.data['Variant Price'] || '0');
return (currentPrice * 1.1).toFixed(2);
}
);
// 2. Find and resolve duplicate images
const duplicates = findDuplicateImages(products);
console.log('Duplicate images found:', duplicates);
// 3. Bulk assign images to variants based on rules
for (const handle in products) {
const product = products[handle];
const rules: ImageAssignmentRule<{}>[] = [
{
matcher: (variant) => variant.data['Option1 Value'] === 'Red',
getImageSrc: () => 'https://example.com/red-variant.jpg'
},
{
matcher: (variant) => variant.data['Option1 Value'] === 'Blue',
getImageSrc: () => 'https://example.com/blue-variant.jpg'
}
];
assignBulkImagesToVariants(product, rules);
}
// 4. Find variants across all products
const expensiveVariants = findAllVariants(products, (variant) => {
const price = parseFloat(variant.data['Variant Price'] || '0');
return price > 100;
});
// 5. Find and fix images without alt text
const imagesWithoutAlt = findImagesWithoutAltText(products);
imagesWithoutAlt.forEach(({ image, product }) => {
image.alt = `${product.data.Title} product image`;
});
// 6. Clean up orphaned images
for (const handle in products) {
const orphanedImages = findOrphanedImages(products[handle]);
console.log(`Product ${handle} has ${orphanedImages.length} orphaned images`);
}
// 7. Convert to arrays for analysis
const allVariants = toVariantArray(products);
const allImages = toImageArray(products);
console.log(`Total variants: ${allVariants.length}`);
console.log(`Total images: ${allImages.length}`);
await writeShopifyCSV(products, 'updated-products.csv');
}
```
### Modifying Data (CRUD)
These helpers allow you to programmatically create or update products, variants, and more.
```typescript
import {
parseShopifyCSV,
writeShopifyCSV,
createProduct,
addVariant,
addMetafieldColumn,
setMetafieldValue
} from 'parse-shopify-csv';
async function addNewProduct() {
const products = await parseShopifyCSV('shopify-export.csv');
// 1. Create a new, empty metafield column for all products
addMetafieldColumn(products, {
namespace: 'custom',
key: 'material',
type: 'string',
defaultValue: 'N/A'
});
// 2. Create a new product
const newProduct = createProduct('my-new-jacket', {
Title: 'The All-Weather Jacket',
Vendor: 'MyBrand',
Status: 'draft'
});
// Add it to the main collection
products[newProduct.data.Handle] = newProduct;
// or use addProduct(products, newProduct);
// 3. Add variants to the new product
addVariant(newProduct, {
options: { Size: 'M', Color: 'Black' },
'Variant SKU': 'JKT-BLK-M',
'Cost per item': '99.50'
});
// 4. Set a value for its new metafield
setMetafieldValue(newProduct, 'custom', 'material', 'Gore-Tex');
await writeShopifyCSV('export-with-new-product.csv', products);
}
```
### Querying and Finding Data
These helpers allow you to efficiently search and filter your product data.
```typescript
import {
parseShopifyCSV,
findProducts,
findProductsByMetafield,
findImagesWithoutAltText
} from 'parse-shopify-csv';
async function runAudits() {
const products = await parseShopifyCSV('shopify-export.csv');
// Example 1: Find all products from a specific vendor
const brandProducts = findProducts(products, p => p.data.Vendor === 'MyBrand');
console.log(`Found ${brandProducts.length} products from MyBrand.`);
// Example 2: Find all products with a specific metafield value
// (e.g., all products where the 'features' list includes 'Waterproof')
const waterproofProducts = findProductsByMetafield(
products, 'custom', 'features',
(val) => Array.isArray(val) && val.includes('Waterproof')
);
console.log(`Found ${waterproofProducts.length} waterproof products.`);
// Example 3: Perform an SEO audit to find images missing alt text
const imagesToFix = findImagesWithoutAltText(products);
if (imagesToFix.length > 0) {
console.log(`Found ${imagesToFix.length} images missing alt text.`);
}
}
```
#### Price Management & Formatting
```typescript
import {
parseShopifyCSV,
writeShopifyCSV,
parsePrice,
stringifyPrice,
normalizePrice,
adjustPrice,
updateVariantPrice,
bulkUpdateVariantField,
minPrice,
maxPrice,
averagePrice
} from 'parse-shopify-csv';
async function managePricing() {
const products = await parseShopifyCSV('shopify-export.csv');
// 1. Parse prices from various formats
const price1 = parsePrice("$29.99"); // 29.99
const price2 = parsePrice("29,99"); // 29.99 (European format)
const price3 = parsePrice("1,234.56"); // 1234.56 (with thousands separator)
const price4 = parsePrice("FREE"); // 0
// 2. Format prices to Shopify CSV format
const formattedPrice = stringifyPrice(29.99); // "29.99"
const wholeDollar = stringifyPrice(30); // "30.00"
const threeDecimals = stringifyPrice(29.999, 3); // "29.999"
// 3. Normalize mixed price formats
const normalizedPrices = [
normalizePrice("$29.99"), // "29.99"
normalizePrice("30,00"), // "30.00"
normalizePrice("1,234.56"), // "1234.56"
];
// 4. Apply bulk price adjustments
const discountedProducts = bulkUpdateVariantField(
products,
'Variant Price',
(variant, product) => adjustPrice(variant.data['Variant Price'], -10, 'percentage')
);
// 5. Update individual variant prices safely
for (const handle in products) {
const product = products[handle];
for (const variant of product.variants) {
// Apply 15% markup, with validation
const success = updateVariantPrice(
variant,
adjustPrice(variant.data['Variant Price'], 15, 'percentage')
);
if (!success) {
console.warn(`Failed to update price for SKU: ${variant.data['Variant SKU']}`);
}
}
}
// 6. Price analysis across products
const allPrices = [];
for (const product of Object.values(products)) {
for (const variant of product.variants) {
allPrices.push(variant.data['Variant Price']);
}
}
console.log('Price Analysis:');
console.log(`Minimum price: ${minPrice(allPrices)}`);
console.log(`Maximum price: ${maxPrice(allPrices)}`);
console.log(`Average price: ${averagePrice(allPrices)}`);
await writeShopifyCSV('updated-pricing.csv', products);
}
```
<br />
## Flexible Schema Support & Type Generation
This library automatically adapts to various Shopify CSV export formats, including different column structures, market-specific pricing, Google Shopping fields, and extensive metafields.
### Automatic Schema Detection
The library automatically detects and handles:
- **Market-Specific Pricing**: `Price / United States`, `Price / International`, `Compare At Price / Canada`, etc.
- **Google Shopping Fields**: All Google Shopping columns with varying configurations
- **Metafields**: Both user-friendly format (`Field Name (product.metafields.namespace.key)`) and standard format (`Metafield: namespace.key[type]`)
- **Custom Fields**: Any additional columns not part of standard Shopify schema
```typescript
import { parseShopifyCSVFromString, detectCSVSchema } from 'parse-shopify-csv';
// Analyze your CSV structure
const csvHeaders = ['Handle', 'Title', 'Price / United States', 'Metal (product.metafields.product.metal)', ...];
const schema = detectCSVSchema(csvHeaders, {
detectMarketPricing: true,
detectGoogleShopping: true,
detectVariantFields: true
});
console.log(schema);
// {
// totalColumns: 45,
// coreFields: 7,
// marketPricingFields: 3,
// googleShoppingFields: 6,
// metafieldColumns: 12,
// customFields: 2
// }
```
### TypeScript Interface Generation
Generate type-safe TypeScript interfaces directly from your CSV headers:
```typescript
import { generateTypeScriptInterface, getCSVHeadersFromString } from 'parse-shopify-csv';
const csvData = `Handle,Title,Vendor,Metal (product.metafields.product.metal),Chain Length (product.metafields.product.chain_length)
jewelry-item,Silver Necklace,Premium Co,Sterling Silver,18 inches`;
// Extract headers and generate TypeScript interface
const headers = getCSVHeadersFromString(csvData);
const tsInterface = generateTypeScriptInterface(headers, 'JewelrySchema');
console.log(tsInterface);
// interface JewelrySchema {
// "Handle": string;
// "Title": string;
// "Vendor": string;
// "Metal (product.metafields.product.metal)"?: string;
// "Chain Length (product.metafields.product.chain_length)"?: string;
// }
```
### Zod Schema Generation
Generate runtime validation schemas using Zod:
```typescript
import { generateZodSchema } from 'parse-shopify-csv';
const zodSchema = generateZodSchema(headers, 'JewelrySchema');
console.log(zodSchema);
// const JewelrySchema = z.object({
// "Handle": z.string(),
// "Title": z.string(),
// "Vendor": z.string(),
// "Metal (product.metafields.product.metal)": z.string().optional(),
// "Chain Length (product.metafields.product.chain_length)": z.string().optional(),
// });
```
### Complete Analysis & Schema Generation
For comprehensive analysis of your CSV structure:
```typescript
import { analyzeCSVAndGenerateSchemas } from 'parse-shopify-csv';
const analysis = analyzeCSVAndGenerateSchemas(csvData, {
interfaceName: 'MyProductSchema',
zodSchemaName: 'MyProductSchema',
schemaDetectionOptions: {
detectMarketPricing: true,
detectGoogleShopping: true,
detectVariantFields: true
}
});
console.log('Headers:', analysis.headers);
console.log('Schema:', analysis.detectedSchema);
console.log('TypeScript:', analysis.typeScript);
console.log('Zod:', analysis.zodSchema);
```
### Working with Complex Metafields
The library handles various metafield formats automatically:
```typescript
// Your CSV with extensive metafields
const complexCSV = `Handle,Title,Metal (product.metafields.product.metal),Chain Length (product.metafields.product.chain_length),Jewelry Material (product.metafields.shopify.jewelry-material)
necklace,Silver Heart Necklace,Sterling Silver,18 inches,Sterling Silver`;
const products = await parseShopifyCSVFromString(complexCSV);
const product = products['necklace'];
// Access metafields via structured object (when available)
if (product.metafields.product) {
console.log('Metal:', product.metafields.product.metal?.value);
console.log('Chain Length:', product.metafields.product.chain_length?.value);
}
// Or access via raw data columns
console.log('Metal:', product.data['Metal (product.metafields.product.metal)']);
console.log('Chain Length:', product.data['Chain Length (product.metafields.product.chain_length)']);
```
### Market-Specific Pricing Utilities
Built-in utilities for international pricing:
```typescript
import { extractMarketPricing, setMarketPricing, getAvailableMarkets } from 'parse-shopify-csv';
// Extract all market pricing from a product
const marketPrices = extractMarketPricing(product.data);
console.log(marketPrices);
// {
// 'United States': { price: '29.99', compareAtPrice: '39.99' },
// 'International': { price: '34.99', compareAtPrice: '44.99' }
// }
// Set pricing for a specific market
setMarketPricing(product.data, 'Canada', {
price: '39.99',
compareAtPrice: '49.99'
});
// Get all available markets in your data
const markets = getAvailableMarkets(products);
console.log('Available markets:', markets);
```
### CSV Header Utilities
Extract and analyze CSV headers:
```typescript
import { getCSVHeaders, getCSVHeadersFromString } from 'parse-shopify-csv';
// From file
const headersFromFile = await getCSVHeaders('products.csv');
// From string
const headersFromString = getCSVHeadersFromString(csvData);
console.log('Total columns:', headersFromString.length);
console.log('First 5 headers:', headersFromString.slice(0, 5));
```
## API Reference
### Core Functions
- **`parseShopifyCSV<T>(path)`**: Parses a Shopify product CSV from a file path.
- **`parseShopifyCSVFromString<T>(csv)`**: Parses a Shopify product CSV from a string.
- **`stringifyShopifyCSV(parsedData)`**: Converts the structured product data back into a CSV formatted string.
- **`writeShopifyCSV(path, parsedData)`**: A convenient wrapper that stringifies data and writes it to a file.
### Schema Detection & Type Generation
- **`detectCSVSchema(headers, options?)`**: Analyzes CSV headers and returns schema information including core fields, metafields, Google Shopping fields, and market pricing fields.
- **`getCSVHeaders(filePath)`**: Extracts column headers from a CSV file.
- **`getCSVHeadersFromString(csvString)`**: Extracts column headers from a CSV string.
- **`generateTypeScriptInterface(headers, interfaceName?, options?)`**: Generates a TypeScript interface definition from CSV headers.
- **`generateZodSchema(headers, schemaName?, options?)`**: Generates a Zod validation schema from CSV headers.
- **`analyzeCSVAndGenerateSchemas(csvData, options?)`**: Complete analysis that returns headers, detected schema, TypeScript interface, and Zod schema.
### Market Pricing Utilities
- **`extractMarketPricing(productData)`**: Extracts all market-specific pricing fields from a product's data.
- **`setMarketPricing(productData, market, pricing)`**: Sets pricing for a specific market on a product.
- **`getAvailableMarkets(products)`**: Returns all available markets found across the product collection.
### Validation & Helper Functions
- **`validateCoreFields(productData)`**: Validates that a product data object contains all required core Shopify fields.
- **`createMinimalProductRow(options)`**: Creates a minimal product row with required fields for CSV export.
### Utility Functions (CRUD)
These functions are for creating, updating, or deleting individual items on a product.
- **`createProduct(handle, data)`**: Creates a new, minimal product object.
- **`addProduct(products, product)`**: Adds a new product to the collection.
- **`deleteProduct(products, handle)`**: Deletes a product from the collection.
- **`addVariant(product, data)`**: Adds a new variant to a product.
- **`removeVariant(product, sku)`**: Removes a single variant from a product by its SKU.
- **`addImage(product, data)`**: Adds a new image to a product's image collection.
- **`assignImageToVariant(product, imageSrc, sku)`**: Assigns an existing image to a specific variant.
- **`addMetafieldColumn(products, options)`**: Creates a new metafield column for all products in a collection.
- **`setMetafieldValue(product, ns, key, value)`**: Sets the value of an existing metafield on a product.
- **`getMetafield(product, ns, key)`**: Retrieves a metafield object from a product.
### Utility Functions (Querying)
These functions are for finding items based on specific criteria.
- **`findProduct<T>(products, predicate)`**: Finds the first product matching a condition.
- **`findProducts<T>(products, predicate)`**: Finds all products matching a condition.
- **`findProductsByMetafield<T>(products, ns, key, value)`**: Finds all products with a specific metafield value.
- **`findProductsMissingMetafield<T>(products, ns, key)`**: Finds all products that do not have a specific metafield defined.
### Functional Utilities
These functions provide common functional programming helpers to process the entire product collection in an immutable way.
- **`map<T, R>(products, callback, shouldClone = true)`**: Iterates over each product, applies a `callback` function to it, and returns a new collection of the results. Supports type transformation from `T` to `R`. By default, a deep clone of each product is passed to the callback to prevent side effects.
- **`filter<T>(products, predicate, shouldClone = true)`**: Creates a new product collection containing only the products for which the `predicate` function returns `true`. Preserves generic type information. By default, a deep clone of each product is passed to the predicate.
- **`reduce<A, T>(products, callback, initialValue, shouldClone = true)`**: Executes a `reducer` function on each product of the collection, resulting in a single output value. Maintains generic type information throughout the reduction. By default, a deep clone of each product is passed to the callback.
- **`toArray<T>(products)`**: Converts a product collection into a plain array of products. Useful for compatibility with array methods or when you need indexed access.
- **`countProducts<T>(products)`**: Returns the total number of products in the collection.
- **`countVariants<T>(products)`**: Returns the total number of variants across all products.
- **`countImages<T>(products)`**: Returns the total number of images across all products.
- **`countProductsWhere<T>(products, predicate)`**: Returns the count of products that match the given predicate function.
- **`countVariantsWhere<T>(products, predicate)`**: Returns the count of variants that match the given predicate function.
- **`countProductsWithTag<T>(products, tag)`**: Returns the count of products that have the specified tag.
- **`countProductsByType<T>(products)`**: Returns an object mapping product types to their counts.
- **`countProductsByVendor<T>(products)`**: Returns an object mapping vendors to their product counts.
- **`getCollectionStats<T>(products)`**: Returns comprehensive statistics about the collection including totals, averages, and breakdowns by type/vendor/tags.
### Advanced & Bulk Utilities
These functions perform complex, task-oriented operations across multiple products, perfect for scripting and data migrations.
#### Bulk Operations
- **`bulkUpdatePrices<T>(products: TypedProduct<T>[], options)`**: Updates prices for many products at once (e.g., for applying a store-wide sale). Takes an array of products.
- **`bulkFindAndReplace<T>(products: TypedProduct<T>[], field, find, replaceWith)`**: Performs a find-and-replace on a text field across multiple products. Takes an array of products.
- **`bulkUpdateVariantField<T>(products, field, value)`**: Updates a specific field across all variants in multiple products. Supports both static values and dynamic functions.
- **`bulkUpdateInventory<T>(products, updates)`**: Mass inventory updates using SKU-to-quantity mapping with type preservation.
#### Inventory Management
Utilities for managing product inventory quantities and tracking with full type safety.
- **`updateInventoryQuantity<T>(product, variantSKU, quantity)`**: Updates inventory quantity for a specific variant by SKU.
#### Advanced Variant Management
Enhanced utilities for working with product variants across multiple products with type preservation.
- **`findVariant<T>(product, sku)`**: Finds a specific variant within a product by SKU.
- **`addVariant<T>(product, newVariantData)`**: Adds a new variant to a product with proper option handling.
- **`removeVariant<T>(product, sku)`**: Removes a variant from a product by SKU.
- **`findVariantByOptions<T>(product, optionsToMatch)`**: Finds a variant by matching option values.
- **`findAllVariants<T>(products, predicate)`**: Finds all variants across all products that match a predicate.
- **`toVariantArray<T>(products)`**: Converts all variants from all products into a flat array with product context.
#### Advanced Image Management
Enhanced utilities for managing product images and variant assignments.
- **`findDuplicateImages<T>(products)`**: Finds images used by multiple products (useful for optimization).
- **`assignBulkImagesToVariants<T>(product, rules)`**: Assigns images to variants based on configurable rules.
- **`addImage<T>(product, newImageData)`**: Adds a new image to a product (prevents duplicates).
- **`assignImageToVariant<T>(product, imageSrc, sku)`**: Assigns an existing image to a specific variant.
- **`findImagesWithoutAltText<T>(products)`**: Finds all images across products that lack alt text.
- **`findOrphanedImages<T>(product)`**: Finds images in a product that aren't assigned to any variant.
- **`toImageArray<T>(products)`**: Converts all images from all products into a flat array with product context.
#### Product Organization
Utilities for categorizing and organizing products with custom field type safety.
#### Tag Management
Modern, comprehensive utilities for managing product tags with automatic deduplication and case-insensitive operations.
- **`getTags<T>(product)`**: Gets all tags for a product as an array of strings.
- **`hasTag<T>(product, tag)`**: Checks if a product has a specific tag (case-insensitive).
- **`addTag<T>(product, tag)`**: Adds a single tag with automatic deduplication.
- **`removeTag<T>(product, tag)`**: Removes a tag (case-insensitive).
- **`setTags<T>(product, tags)`**: Replaces all tags with new ones (accepts array or comma-separated string).
- **`addTags<T>(product, tags)`**: Adds multiple tags at once.
- **`removeTags<T>(product, tags)`**: Removes multiple tags at once.
- **`hasAllTags<T>(product, tags)`**: Checks if product has all specified tags.
- **`hasAnyTag<T>(product, tags)`**: Checks if product has any of the specified tags.
- **`findProductsByTag<T>(products, tag)`**: Finds all products with a specific tag.
- **`findProductsByTags<T>(products, tags)`**: Finds products with all specified tags.
- **`getAllTags<T>(products)`**: Gets all unique tags across all products with type preservation.
- **`getTagStats<T>(products)`**: Returns tag usage statistics while maintaining generic type information.
- **`parseTags(tagsString)`**: Parses comma-separated tags string into array.
- **`serializeTags(tags)`**: Converts tag array to comma-separated string.
#### Inventory Management
Modern utilities for managing product inventory and stock levels.
- **`updateInventoryQuantity<T>(product, variantSKU, quantity)`**: Updates inventory quantity for a specific variant by SKU.
#### Advanced Variant Management
Enhanced utilities for bulk variant operations and management.
- **`bulkUpdateVariantField<T>(products, field, value)`**: Bulk updates a specific field across all variants (supports static values or functions).
- **`findVariant<T>(product, sku)`**: Finds a specific variant within a product by SKU.
- **`addVariant<T>(product, newVariantData)`**: Adds a new variant to a product with proper option handling.
- **`removeVariant<T>(product, sku)`**: Removes a variant from a product by SKU.
- **`findVariantByOptions<T>(product, optionsToMatch)`**: Finds a variant by matching option values.
- **`findAllVariants<T>(products, predicate)`**: Finds all variants across all products that match a predicate.
- **`toVariantArray<T>(products)`**: Converts all variants from all products into a flat array with product context.
#### Advanced Image Management
Enhanced utilities for managing product images and variant assignments.
- **`findDuplicateImages<T>(products)`**: Finds images used by multiple products (useful for optimization).
- **`assignBulkImagesToVariants<T>(product, rules)`**: Assigns images to variants based on configurable rules.
- **`addImage<T>(product, newImageData)`**: Adds a new image to a product (prevents duplicates).
- **`assignImageToVariant<T>(product, imageSrc, sku)`**: Assigns an existing image to a specific variant.
- **`findImagesWithoutAltText<T>(products)`**: Finds all images across products that lack alt text.
- **`findOrphanedImages<T>(product)`**: Finds images in a product that aren't assigned to any variant.
- **`toImageArray<T>(products)`**: Converts all images from all products into a flat array with product context.
#### Product Organization & Categorization
Utilities for organizing and categorizing products.
- **`findUncategorizedProducts<T>(products, config)`**: Finds products that don't meet categorization criteria (missing required fields, tags, metafields, or custom conditions).
#### Data Validation & Cleanup
- **`findDuplicateSKUs<T>(products)`**: Scans the entire collection for duplicate variant SKUs to prevent Shopify import errors.
- **`sanitizeHandle(input)`**: Cleans a string (like a product title) to make it a valid, URL-safe Shopify handle.
- **`removeMetafieldColumn<T>(products, namespace, key)`**: Completely removes a metafield column from all products in the collection.
#### Price Utilities
Comprehensive utilities for parsing, formatting, and manipulating prices in Shopify CSV format.
- **`parsePrice(priceString)`**: Parses various price formats into numbers. Handles currency symbols, different decimal separators, thousands separators, and special cases like "FREE".
- **`stringifyPrice(price, decimalPlaces?)`**: Formats numbers as Shopify-compatible price strings with proper decimal formatting.
- **`isValidPrice(priceString)`**: Validates if a price string is in correct Shopify CSV format (no symbols, dot decimal separator).
- **`normalizePrice(price, decimalPlaces?)`**: Converts any price format to standard Shopify CSV format.
- **`updateVariantPrice(variant, newPrice, field?)`**: Safely updates variant price with automatic parsing and validation.
- **`updateVariantCompareAtPrice(variant, newPrice)`**: Updates variant compare-at price with validation.
- **`adjustPrice(originalPrice, adjustment, type)`**: Calculates price adjustments (percentage or fixed amount) with proper formatting.
- **`comparePrice(price1, price2)`**: Compares two prices handling different input formats.
- **`minPrice(prices[])`**: Finds minimum price from an array of various price formats.
- **`maxPrice(prices[])`**: Finds maximum price from an array of various price formats.
- **`averagePrice(prices[])`**: Calculates average price from an array of various price formats.
#### Product Lifecycle
- **`cloneProduct<T>(productToClone, newHandle, newTitle)`**: Creates a deep clone of a product, including its variants, images, and metafields, under a new handle and title. Preserves custom type information.
### Custom Error
- **`CSVProcessingError`**: A custom error class thrown for all library-specific errors, allowing for targeted `catch` blocks.
<br />
## Advanced Type Safety Features
### Generic Type Utilities
The library provides advanced type utilities to enhance your development experience:
```typescript
import {
DefineCustomColumns,
DefineMetafields,
CombineColumnsAndMetafields,
TypedProduct,
ProductsCollection
} from 'parse-shopify-csv';
// Define your business-specific column types
type MyCustomColumns = DefineCustomColumns<{
'Supplier SKU': string;
'Cost Basis': string;
'Margin Target': string;
'Internal Category': string;
}>;
// Define your metafield structure
type MyMetafields = DefineMetafields<{
'custom.material': string;
'custom.features': string[];
'inventory.reorder_point': string;
}>;
// Combine for complete schema
type MyCompleteSchema = CombineColumnsAndMetafields<MyCustomColumns, MyMetafields>;
// Use throughout your application
const products = await parseShopifyCSV<MyCompleteSchema>('products.csv');
```
### Type-Safe Business Logic
Create strongly-typed business logic that prevents runtime errors:
```typescript
// Type-safe predicate functions
const premiumProductPredicate: TypedProductPredicate<MyCompleteSchema> = (product) => {
return product.data['Internal Category'] === 'Premium' && // ✅ Autocomplete!
product.data['Supplier SKU'] !== ''; // ✅ Type-safe!
};
// Type-safe transformations
const enrichedProducts = map(products, (product: TypedProduct<MyCompleteSchema>) => {
// Full autocomplete for all your custom fields
const category = product.data['Internal Category'];
const supplierSku = product.data['Supplier SKU'];
// Apply business logic with compile-time safety
if (category === 'Premium') {
addTag(product, 'premium-tier');
}
return product; // Type information preserved!
});
```
### Gradual Type Adoption
Start with flexible typing and add more specificity as needed:
```typescript
// Start loose - works with any CSV structure
const anyProducts = await parseShopifyCSV('unknown-structure.csv');
// Add typing for specific operations
type KnownFields = DefineCustomColumns<{
'Important Field': string;
}>;
// Cast when you know the structure
const typedProduct = anyProducts['some-handle'] as TypedProduct<KnownFields>;
console.log(typedProduct.data['Important Field']); // ✅ Now type-safe!
```
## Default Export
The library's default export is an object containing the core functions for convenience:
- `parse`: Alias for `parseShopifyCSV`.
- `write`: Alias for `writeShopifyCSV`.
- `stringify`: Alias for `stringifyShopifyCSV`.
- `parseFromString`: Alias for `parseShopifyCSVFromString`.
This allows for a more concise import style:
```typescript
import shopifyCSV from 'parse-shopify-csv';
async function process(file: string) {
const products = await shopifyCSV.parse(file);
// ...
await shopifyCSV.write('new-file.csv', products);
}
```
<br />
## Key Data Structures
### `ShopifyProductCSVParsedRow<T>`
The main object for a single, fully parsed product.
- `data: ShopifyProductCSV<T>`: The full data from the product's first row. **This object is automatically updated when you modify `metadata.parsedValue`**.
- `metadata: ShopifyProductMetafields`: An iterable object containing all parsed metafields.
- `images: ShopifyCSVParsedImage[]`: An array of all unique product images.
- `variants: ShopifyCSVParsedVariant[]`: An array of all product variants.
### `ShopifyMetafield`
The structure for each entry within `product.metadata`.
- `key: string`: The short key of the metafield (e.g., `fabric`).
- `namespace: string`: The namespace of the metafield (e.g., `my_fields`).
- `isList: boolean`: True if the metafield type is a list (e.g., `list.single_line_text_field`).
- `value: string`: The raw string value directly from the CSV cell.
- `parsedValue: string | string[]`: The parsed value (an array for lists, a string otherwise). **Assigning a new value to this property automatically updates the underlying `product.data` object**, ensuring your changes are saved.
### Enhanced Generic Types
The library provides several utility types to improve your development experience:
- **`DefineCustomColumns<T>`**: Type helper for defining your custom CSV columns with full autocomplete.
- **`DefineMetafields<T>`**: Type helper for defining metafields with their expected types (string or string[]).
- **`CombineColumnsAndMetafields<C, M>`**: Combines custom columns and metafields into a complete schema.
- **`TypedProduct<T>`**: Alias for `ShopifyProductCSVParsedRow<T>` that preserves your custom type information.
- **`ProductsCollection<T>`**: Enhanced collection type that maintains generic information and iterability.
- **`TypedProductPredicate<T>`**: Predicate function type that preserves generic information for filtering.
- **`TypedVariantPredicate<T>`**: Variant predicate type with preserved generic information.
## Gotchas & Troubleshooting
### **Mutability and Side Effects**
Most utility functions **mutate the original objects** by default for performance. If you need immutable operations, clone the objects first:
```typescript
// ❌ This modifies the original product
addTag(product, 'new-tag');
// ✅ This preserves the original
const productCopy = structuredClone(product);
addTag(productCopy, 'new-tag');
// ✅ Or use functional utilities with cloning enabled (default)
const newProducts = map(products, (p) => {
addTag(p, 'new-tag');
return p;
}, true); // shouldClone = true (default)
```
**Exception:** The functional utilities (`map`, `filter`, `reduce`) clone by default unless you explicitly set `shouldClone = false`.
### **Tags Serialization**
Tags are stored as comma-separated strings in the CSV, but the tag utilities handle this automatically:
```typescript
// ✅ These are equivalent
product.data.Tags = 'summer, sale, featured';
setTags(product, ['summer', 'sale', 'featured']);
// ❌ Don't manually manipulate the Tags string
product.data.Tags += ', new-tag'; // Can create duplicates and formatting issues
// ✅ Use tag utilities instead
addTag(product, 'new-tag');
```
### **Metadata Syncing**
Changes to `metadata.parsedValue` automatically sync to the raw CSV data, but direct data changes don't sync back:
```typescript
// ✅ This syncs automatically
product.metadata['Metafield: custom.color[string]'].parsedValue = 'Blue';
console.log(product.data['Metafield: custom.color[string]']); // 'Blue'
// ❌ This doesn't sync to metadata
product.data['Metafield: custom.color[string]'] = 'Red';
console.log(product.metadata['Metafield: custom.color[string]'].parsedValue); // Still 'Blue'
// ✅ Use utility functions for consistency
setMetafieldValue(product, 'custom', 'color', 'Red');
```
### **Product vs. Variant Metadata**
Products and variants have separate metadata objects:
```typescript
// Product-level metafield
setMetafieldValue(product, 'custom', 'brand', 'Nike');
// Variant-level metafield (if supported by your setup)
// Note: Standard Shopify CSV doesn't support variant metafields
// You'd need custom columns for this
```
### **Handle Overwrites in createProduct**
The `createProduct` function sets the handle in the data object, potentially overwriting any handle passed in the second parameter:
```typescript
// ❌ The handle in productData will be ignored
const product = createProduct('correct-handle', {
Handle: 'ignored-handle', // This will be overwritten
Title: 'My Product'
});
console.log(product.data.Handle); // 'correct-handle'
// ✅ Only pass the handle as the first parameter
const product = createProduct('my-product-handle', {
Title: 'My Product'
});
```
### **Products Collection is Not an Array**
The parsed products collection is an iterable object, not an array:
```typescript
const products = await parseShopifyCSV('file.csv');
// ✅ These work
for (const product of products) { }
const productArray = Object.values(products);
const handles = Object.keys(products);
// ❌ These don't work
products.map(p => p.data.Title); // Error: products.map is not a function
products.length; // undefined
products[0]; // undefined (unless you have a product with handle '0')
// ✅ Use functional utilities (maintains type information)
const titles = map(products, p => p.data.Title);
// or
const titles = Object.values(products).map(p => p.data.Title);
// ✅ Type-safe conversion to array
const productArray: TypedProduct<MySchema>[] = Object.values(products);
```
### **Working with Custom Types**
When using custom column types, follow these patterns for the best experience:
```typescript
// Define your schema once
type MySchema = DefineCustomColumns<{
'Custom Price': string;
'Supplier Code': string;
}>;
// Use consistently throughout your app
const products = await parseShopifyCSV<MySchema>('file.csv');
const filtered = filter<MySchema>(products, p => p.data['Custom Price'] !== '');
const mapped = map<MySchema>(products, p => { /* transform */ return p; });
// ❌ Don't mix typed and untyped
const mixed: Record<string, ShopifyProductCSVParsedRow> = products; // Loses type info
// ✅ Maintain type consistency
const consistent: ProductsCollection<MySchema> = products; // Preserves types
```
### **Generic Type Information