ProvenPin Install

Streaming CSV transform with csv-parse + csv-stringify: working versions (tested 2026-10-15)

Transform large CSV files without loading them into memory: csv-parse in columns mode, a Node Transform for row logic, csv-stringify output, wired with stream/promises pipeline.

passed3 passed · 0 failed2026-10-15 13:27 UTC
Status
passed
Last passed
2026-10-15 13:27 UTC
Tests
3 / 3
Language
TypeScript
Runtime
node v22.23.3
pinned versions4
csv-parse@7.0.3csv-stringify@6.9.0tsx@4.23.15@types/node@24.19.2
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38742087302

The code

Exactly the files that passed. Your agent can pull them with get_recipe("csv-parse-stringify-streams") (install), or see them on GitHub.

package.json
{
  "name": "csv-parse-stringify-streams",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "csv-parse": "7.0.3",
    "csv-stringify": "6.9.0"
  },
  "devDependencies": {
    "tsx": "4.23.15",
    "@types/node": "24.19.2"
  }
}
src/transform.ts
import { parse } from "csv-parse";
import { stringify } from "csv-stringify";
import { Transform, type Readable, type Writable } from "node:stream";
import { pipeline } from "node:stream/promises";

type Row = { sku: string; qty: string; price: string };

// Adds a total column, drops bad rows. Constant memory for any file size.
export async function addTotals(input: Readable, output: Writable) {
  const stats = { kept: 0, dropped: 0 };
  const logic = new Transform({
    objectMode: true,
    transform(row: Row, _enc, cb) {
      const qty = Number(row.qty), price = Number(row.price);
      if (!qty || Number.isNaN(price)) { stats.dropped++; return cb(); }
      stats.kept++;
      cb(null, { sku: row.sku, qty, price: price.toFixed(2), total: (qty * price).toFixed(2) });
    },
  });
  await pipeline(
    input,
    parse({ columns: true, trim: true, skip_empty_lines: true, bom: true }),
    logic,
    stringify({ header: true, columns: ["sku", "qty", "price", "total"] }),
    output,
  );
  return stats;
}
test/transform.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { Readable, Writable } from "node:stream";
import { addTotals } from "../src/transform.ts";

function sink() {
  const chunks: string[] = [];
  return { out: new Writable({ write(c, _e, cb) { chunks.push(c.toString()); cb(); } }), text: () => chunks.join("") };
}

test("adds totals and drops bad rows", async () => {
  const s = sink();
  const stats = await addTotals(Readable.from(["sku,qty,price\n", "A, 2 ,1.5\n", "B,0,9\n", "C,1,abc\n", "\n", "D,3,2\n"]), s.out);
  assert.deepEqual(stats, { kept: 2, dropped: 2 });
  assert.equal(s.text(), "sku,qty,price,total\nA,2,1.50,3.00\nD,3,2.00,6.00\n");
});

test("handles a large generated file as a stream", async () => {
  async function* rows() { yield "sku,qty,price\n"; for (let i = 0; i < 50_000; i++) yield `S${i},1,1\n`; }
  assert.equal((await addTotals(Readable.from(rows()), sink().out)).kept, 50_000);
});

test("malformed CSV rejects the pipeline", async () => {
  await assert.rejects(addTotals(Readable.from(['sku,qty,price\n"A,1,1\n']), sink().out));
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.