Skip to content

Latest commit

 

History

History
191 lines (149 loc) · 6.78 KB

File metadata and controls

191 lines (149 loc) · 6.78 KB

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";

const importer = new CsvImporter();

// Simple products — no continuation rows allowed
importer.addDefinition(
    ProductDefinition.create("simple")
        .matchWhen((row) => row.type === "simple")
        .requireFields(["handle", "title", "type", "sku"]),
);

// Variable products — each variant is a continuation row
importer.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 row
importer.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"]),
        ),
);

const result = importer.parse(csvString);

if (result.ok) {
    for (const group of result.valid) {
        console.log(
            group.handle,
            group.definition,
            group.head,
            group.continuations,
        );
    }
} else {
    for (const error of result.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 src
simple-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:

const importer = new CsvImporter()
  .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:

interface ParseResult {
    ok: boolean; // true if zero errors
    valid: ValidGroup[]; // groups that passed validation
    errors: ValidationError[];
}

interface ValidGroup {
    definition: string; // name of the matched definition
    handle: string;
    head: RawRow;
    continuations: RawRow[];
}

interface ValidationError {
    handle: string;
    line: number;
    field?: ProductRowField; // set when the error is field-specific
    message: string;
}

ProductDefinition.create(name: string)

Starts a new product definition builder.

Method Description
.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.

Method Description
.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:

Field Field Field
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 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