Skip to content

Repository files navigation

csv-import-lib

A fluent TypeScript library for parsing and validating product import CSV files. Define rules for each product type, then run your CSV through them — groups are validated, errors are reported per-row with context.

Concepts

Row groups — Rows are grouped by handle. The first row for a handle is the head row; subsequent rows sharing that handle (or rows with a blank handle immediately following) are continuation rows.

Product definitions — You define what a valid group looks like for each product type: which fields are required on the head row, and what kinds of continuation rows are allowed.

Continuation row types — Each continuation row type has a matchWhen predicate, required fields, and forbidden fields. The first matching type wins.

Installation

bun add csv-import-lib # when published# or copy src/ directly into your project

Quick Start

import{CsvImporter,ProductDefinition,ContinuationRow,}from"csv-import-lib";constimporter=newCsvImporter();// Simple products — no continuation rows allowedimporter.addDefinition(ProductDefinition.create("simple").matchWhen((row)=>row.type==="simple").requireFields(["handle","title","type","sku"]),);// Variable products — each variant is a continuation rowimporter.addDefinition(ProductDefinition.create("variable").matchWhen((row)=>row.type==="variable").requireFields(["handle","title","type","sku","option 1 name","option 1 value",]).allowContinuationRows(ContinuationRow.create().label("variant-row").matchWhen((row)=>!!row.sku).requireFields(["handle","sku","option 1 value"]).forbidFields(["title"]),),);// Serialized products — each IMEI is a continuation rowimporter.addDefinition(ProductDefinition.create("serialized").matchWhen((row)=>row.type==="serialized").requireFields(["handle","title","type","sku","imei"]).allowContinuationRows(ContinuationRow.create().label("imei-row").matchWhen((row)=>!!row.imei&&!row.sku).requireFields(["handle","imei"]).forbidFields(["title","type","sku","price","cost"]),),);constresult=importer.parse(csvString);if(result.ok){for(constgroupofresult.valid){console.log(group.handle,group.definition,group.head,group.continuations,);}}else{for(consterrorofresult.errors){console.error(`Line ${error.line} [${error.handle}]${error.field ? ` field:${error.field}` : ""}: ${error.message}`,);}}

CSV Format

The library expects a header row followed by data rows. The handle column groups rows into products.

handle,title,description,type,quantity,price,cost,sku,imei,barcode,category,tags,status,option 1 name,option 1 value,option 2 name,option 2 value,option 3 name,option 3 value,image srcsimple-product,My Product,A description,simple,5,9.99,5.00,SKU-001,,,Electronics,sale,active,,,,,,,variable-product,My Variable,A description,variable,10,9.99,5.00,SKU-002,,,Clothing,,active,Color,Red,,,,,variable-product,,,,8,9.99,5.00,SKU-003,,,,,,,Blue,,,,,serialized-product,My Serialized,A description,serialized,1,199,99,SKU-004,123456789012,,,,,,,,,,,serialized-product,,,,,,,,987654321098,,,,,,,,,,,

Continuation rows can either repeat the handle or leave it blank — both are treated as belonging to the previous group.

API

new CsvImporter()

Creates a new importer instance. addDefinition() returns this so you can chain:

constimporter=newCsvImporter().addDefinition(...).addDefinition(...)

importer.addDefinition(builder: ProductDefinitionBuilder): this

Registers a product definition. Definitions are evaluated in registration order — the first whose matchWhen predicate returns true for a group's head row will own that group.

importer.parse(csv: string): ParseResult

Parses and validates the CSV. Returns:

interfaceParseResult{ok: boolean;// true if zero errorsvalid: ValidGroup[];// groups that passed validationerrors: ValidationError[];}interfaceValidGroup{definition: string;// name of the matched definitionhandle: string;head: RawRow;continuations: RawRow[];}interfaceValidationError{handle: string;line: number;field?: ProductRowField;// set when the error is field-specificmessage: string;}

ProductDefinition.create(name: string)

Starts a new product definition builder.

MethodDescription
.matchWhen(fn)Predicate against the head row. First match wins.
.requireFields(fields[])Fields that must be non-empty on the head row.
.allowContinuationRows(builder)Register a continuation row type. Can be called multiple times for multiple types.

ContinuationRow.create()

Starts a new continuation row type builder.

MethodDescription
.label(name)Human-readable name used in error messages.
.matchWhen(fn)Predicate to identify this row type. First match wins.
.requireFields(fields[])Fields that must be non-empty.
.forbidFields(fields[])Fields that must be empty.

Available Fields

These are the column names recognized by ProductRow:

FieldFieldField
handletitledescription
typequantityprice
costskuimei
barcodecategorytags
statusoption 1 nameoption 1 value
option 2 nameoption 2 valueoption 3 name
option 3 valueimage src

All field names are typed as ProductRowField — passing an invalid field name to requireFields or forbidFields will be caught at compile time.

Running Tests

bun run vitest

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages