first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
# `etl`
Extract-Transform-Load pipeline engine with typed stages.
<!-- begin:auto-docs -->
## Packages in `etl`
1 package.
| Package | Description |
|---|---|
| [`@suaveplan/etl`](./etl/README.md) | Extract-Transform-Load pipeline engine with typed stages, error handling, observable progress, and OTel instrumentation. |
<!-- end:auto-docs -->

View File

@@ -0,0 +1 @@
Extract-Transform-Load pipeline engine with typed stages.

View File

@@ -0,0 +1,46 @@
# @suaveplan/etl - Extract-Transform-Load pipeline engine with typed stages, error handling, observable progress, and OTel instrumentation
## Summary
Extract-Transform-Load pipeline engine with typed stages, error handling, observable progress, and OTel instrumentation.
<!-- begin:auto-docs -->
## Documentation tree
- [pipeline](./src/core/pipeline/pipeline.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [pipeline](./src/core/pipeline/pipeline.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [errors](./src/errors/etl-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [etl-error](./src/errors/etl-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [schemas](./src/schemas/etl.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [etl.schemas](./src/schemas/etl.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [sink](./src/stages/sink/sink.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [sink](./src/stages/sink/sink.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [source](./src/stages/source/source.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [source](./src/stages/source/source.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [transform](./src/stages/transform/transform.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [transform](./src/stages/transform/transform.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [pipeline](./src/core/pipeline/pipeline.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/etl-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [etl-error](./src/errors/etl-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/etl.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [etl.schemas](./src/schemas/etl.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [sink](./src/stages/sink/sink.md) — <!-- begin:auto-docs --> _(no submodules)_- [sink](./src/stages/sink/sink.md) — <!-- begin:auto-docs --> _(no submodules)_- [source](./src/stages/source/source.md) — <!-- begin:auto-docs --> _(no submodules)_- [source](./src/stages/source/source.md) — <!-- begin:auto-docs --> _(no submodules)_- [transform](./src/stages/transform/transform.md) — <!-- begin:auto-docs --> _(no submodules)_- [transform](./src/stages/transform/transform.md) — <!-- begin:auto-docs --> _(no submodules)_- [pipeline](./src/core/pipeline/pipeline.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/etl-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [etl-error](./src/errors/etl-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/etl.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [etl.schemas](./src/schemas/etl.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [sink](./src/stages/sink/sink.md) — <!-- begin:auto-docs --> _(no submodules)_- [sink](./src/stages/sink/sink.md) — <!-- begin:auto-docs --> _(no submodules)_- [source](./src/stages/source/source.md) — <!-- begin:auto-docs --> _(no submodules)_- [source](./src/stages/source/source.md) — <!-- begin:auto-docs --> _(no submodules)_- [transform](./src/stages/transform/transform.md) — <!-- begin:auto-docs --> _(no submodules)_- [transform](./src/stages/transform/transform.md) — <!-- begin:auto-docs --> _(no submodules)_- [pipeline](./src/core/pipeline/pipeline.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/etl-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [etl-error](./src/errors/etl-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/etl.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [etl.schemas](./src/schemas/etl.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [sink](./src/stages/sink/sink.md) — <!-- begin:auto-docs --> _(no submodules)_- [sink](./src/stages/sink/sink.md) — <!-- begin:auto-docs --> _(no submodules)_- [source](./src/stages/source/source.md) — <!-- begin:auto-docs --> _(no submodules)_- [source](./src/stages/source/source.md) — <!-- begin:auto-docs --> _(no submodules)_- [transform](./src/stages/transform/transform.md) — <!-- begin:auto-docs --> _(no submodules)_- [transform](./src/stages/transform/transform.md) — <!-- begin:auto-docs --> _(no submodules)_- [pipeline](./src/core/pipeline/pipeline.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/etl-error.md) — <!-- begin:auto-docs --> _(no submodules)_ - [etl-error](./src/errors/etl-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/etl.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ - [etl.schemas](./src/schemas/etl.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [sink](./src/stages/sink/sink.md) — <!-- begin:auto-docs --> _(no submodules)_ - [sink](./src/stages/sink/sink.md) — <!-- begin:auto-docs --> _(no submodules)_- [source](./src/stages/source/source.md) — <!-- begin:auto-docs --> _(no submodules)_ - [source](./src/stages/source/source.md) — <!-- begin:auto-docs --> _(no submodules)_- [transform](./src/stages/transform/transform.md) — <!-- begin:auto-docs --> _(no submodules)_ - [transform](./src/stages/transform/transform.md) — <!-- begin:auto-docs --> _(no submodules)_
## Modules
- [Pipeline](src/core/pipeline/pipeline.md) — pipeline orchestrator with fluent API
- [Source](src/stages/source/source.md) — array and generator source factories
- [Transform](src/stages/transform/transform.md) — map, filter, validate, flat-map transforms
- [Sink](src/stages/sink/sink.md) — array, null, callback, and batch sinks
- [EtlError](src/errors/etl-error.md) — structured errors with stable codes
- [Schemas](src/schemas/etl.schemas.md) — Zod schemas for pipeline types
- [Types](src/types/etl.types.md) — shared TypeScript interfaces
## Quick Start
```ts
import { createPipeline, arraySource, mapTransform, ArraySink } from "@suaveplan/etl";
const sink = new ArraySink<number>();
const result = await createPipeline({ source: arraySource([1, 2, 3]) })
.pipe(mapTransform((n) => n * 2))
.to(sink)
.run();
console.log(result.stats.loaded); // 3
console.log(sink.records); // [2, 4, 6]
```

View File

@@ -0,0 +1,82 @@
# pipeline
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides `createPipeline`, the primary orchestrator for the ETL engine. A pipeline connects a `Source`, zero or more `Transform` stages, and a `Sink` into a single async execution graph. Records flow from source through transforms to sink with AbortSignal cancellation, per-record error handling, optional bus event emission, and OTel instrumentation.
## Features
- `createPipeline(opts)` — creates a `Pipeline<TIn>` from a source and options
- `.pipe(transform)` — adds a transform stage; returns a new typed `Pipeline<TIn, TNext>`
- `.to(sink)` — attaches a sink; returns a `RunnablePipeline`
- `.run(signal?)` — executes the pipeline; returns `PipelineResult` with stats and non-fatal errors
- `.dryRun(maxRecords?)` — runs with a limited record count for preview/validation
- `onError` callback — returns `"skip" | "abort" | "retry"` per error
- Bus events: `etl:pipeline:started`, `etl:pipeline:completed`, `etl:pipeline:failed`
- OTel span `etl.pipeline.run` wrapping the full execution
## Usage: Basic Example
```ts
import { createPipeline } from "@suaveplan/etl";
import { arraySource } from "@suaveplan/etl";
import { mapTransform } from "@suaveplan/etl";
import { ArraySink } from "@suaveplan/etl";
const sink = new ArraySink<number>();
const result = await createPipeline({ source: arraySource([1, 2, 3]) })
.pipe(mapTransform((n) => n * 2))
.to(sink)
.run();
console.log(result.stats.loaded); // 3
console.log(sink.records); // [2, 4, 6]
```
## Usage: Advanced Example
```ts
import { createPipeline, arraySource, validateTransform, ArraySink, ETL_CODES } from "@suaveplan/etl";
import z from "zod";
const schema = z.object({ id: z.string() });
const sink = new ArraySink();
const result = await createPipeline({
source: arraySource([{ id: "1" }, { id: 2 }, { id: "3" }] as unknown[]),
onError: (err) => err.code === ETL_CODES.VALIDATION_FAILED ? "skip" : "abort",
})
.pipe(validateTransform(schema))
.to(sink)
.run();
console.log(result.stats.loaded); // 2 (one invalid skipped)
console.log(result.errors.length); // 1
```
## API Reference
### `createPipeline<TIn>(opts)`
- `opts.source: Source<TIn>` — extraction source
- `opts.batchSize?: number` — not used internally (BatchSink handles batching)
- `opts.onError?: ErrorHandler` — per-record error callback
- `opts.clock?: Clock` — injectable clock for duration calculation
- `opts.bus?: { publish }` — optional event bus
### `Pipeline<TIn, TOut>`
- `pipe<TNext>(transform): Pipeline<TIn, TNext>`
- `to(sink: Sink<TOut>): RunnablePipeline<TIn, TOut>`
### `RunnablePipeline<TIn, TOut>`
- `run(signal?): Promise<PipelineResult>`
- `dryRun(maxRecords?): Promise<PipelineResult>`
## Implementation Notes
The pipeline accumulates transforms in order without composing them eagerly — each record is passed through the array sequentially on every iteration. This allows clean type inference via the `.pipe()` fluent API. The `dryRun` method installs an internal abort controller that fires after `maxRecords` writes.

View File

@@ -0,0 +1,67 @@
# etl-error
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the structured error type and stable error-code registry for `@suaveplan/etl`. All pipeline failures — extraction errors, transformation errors, load errors, validation failures, and aborts — surface as `EtlError` instances with a machine-readable `code` field, enabling callers to discriminate by stage without fragile message parsing.
## Features
- `ETL_CODES` registry via `@suaveplan/error`'s `defineCodes` — codes namespaced `ETL.*`
- `EtlError` extends `SuaveplanError` for cause-chain traversal and wire serialisation
- Six codes: `SOURCE_FAILED`, `TRANSFORM_FAILED`, `SINK_FAILED`, `PIPELINE_ABORTED`, `VALIDATION_FAILED`, `BATCH_OVERFLOW`
- `EtlCode` union type for exhaustive error handling
## Usage: Basic Example
```ts
import { EtlError, ETL_CODES } from "@suaveplan/etl";
throw new EtlError(
ETL_CODES.VALIDATION_FAILED,
"Record failed schema validation",
{ context: { field: "email", value: "not-an-email" } }
);
```
## Usage: Advanced Example
```ts
import { EtlError, ETL_CODES } from "@suaveplan/etl";
function handleError(err: unknown): void {
if (err instanceof EtlError) {
switch (err.code) {
case ETL_CODES.PIPELINE_ABORTED:
console.log("Pipeline was cancelled");
break;
case ETL_CODES.SOURCE_FAILED:
console.error("Could not read source:", err.cause);
break;
default:
throw err;
}
}
}
```
## API Reference
### `ETL_CODES`
- `ETL_CODES.SOURCE_FAILED` — extraction stage error
- `ETL_CODES.TRANSFORM_FAILED` — transformation stage error
- `ETL_CODES.SINK_FAILED` — load/sink stage error
- `ETL_CODES.PIPELINE_ABORTED` — AbortSignal fired or explicit abort
- `ETL_CODES.VALIDATION_FAILED` — Zod or custom schema validation failure
- `ETL_CODES.BATCH_OVERFLOW` — batch size exceeded configured limit
### `EtlError`
Extends `SuaveplanError<EtlCode>`. Constructor: `(code, message, options?)`.
## Implementation Notes
Error codes are prefixed with `"ETL."` by `defineCodes`, producing strings like `"ETL.SOURCE_FAILED"`. Wrap underlying errors via `options.cause` to preserve the full error chain.

View File

@@ -0,0 +1,50 @@
# etl.schemas
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides Zod validation schemas for the public option and result types in `@suaveplan/etl`. These schemas are used at integration boundaries — for validating pipeline configuration at runtime and for serialising pipeline result statistics for audit trails or dashboards.
## Features
- `PipelineStatsSchema` — validates the `PipelineStats` result including non-negative integer counters and duration
- `PipelineOptionsSchema` — validates configurable options with passthrough for injectable dependencies
## Usage: Basic Example
```ts
import { PipelineStatsSchema } from "@suaveplan/etl";
const raw = JSON.parse(statsJson);
const stats = PipelineStatsSchema.parse(raw);
console.log(stats.loaded);
```
## Usage: Advanced Example
```ts
import { PipelineOptionsSchema } from "@suaveplan/etl";
function validateConfig(config: unknown) {
return PipelineOptionsSchema.safeParse(config);
}
const result = validateConfig({ batchSize: 500 });
if (!result.success) throw new Error("Invalid pipeline config");
```
## API Reference
### `PipelineStatsSchema`
Validates `{ extracted, transformed, loaded, skipped, durationMs }`. All counts are non-negative integers; `durationMs` is a non-negative number.
### `PipelineOptionsSchema`
Validates `{ batchSize? }`. Uses `.passthrough()` for injectable fields like `source`, `clock`, and `bus`.
## Implementation Notes
Injectable dependencies (`Source`, `Sink`, `Transform`, `Clock`, `Bus`) cannot be validated with Zod since they are runtime objects with function members. The schemas cover only the serialisable configuration fields.

View File

@@ -0,0 +1,67 @@
# sink
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides built-in `Sink` implementations for the ETL pipeline load stage. Sinks receive transformed records and persist or collect them. This module ships four implementations: an array collector, a null discard sink, a callback-backed sink, and a batching sink that accumulates records before flushing to an inner sink.
## Features
- `ArraySink<T>` — collects all written records into `sink.records`; ideal for tests
- `nullSink<T>()` — discards all records; useful for dry-run benchmarking
- `callbackSink<T>(fn)` — calls `fn(record)` for each written record
- `BatchSink<T>` — buffers records and flushes them as arrays to an inner `Sink<T[]>` when batch is full or `flush()` is called
- All sinks implement the full `Sink<T>` lifecycle: `open`, `write`, `flush`, `close`
## Usage: Basic Example
```ts
import { ArraySink } from "@suaveplan/etl";
const sink = new ArraySink<number>();
await sink.write(1);
await sink.write(2);
await sink.flush();
console.log(sink.records); // [1, 2]
```
## Usage: Advanced Example
```ts
import { BatchSink, ArraySink } from "@suaveplan/etl";
const inner = new ArraySink<number[]>();
const batch = new BatchSink(100, inner);
for (let i = 0; i < 250; i++) await batch.write(i);
await batch.flush(); // flushes remaining 50
console.log(inner.records.length); // 3 batches: 100 + 100 + 50
```
## API Reference
### `ArraySink<T>`
- `records: T[]` — accumulated records
- `write(record)` — appends to `records`
### `nullSink<T>(name?)`
Discards all writes. `open`, `flush`, `close` are no-ops.
### `callbackSink<T>(fn, name?)`
`fn: (record: T) => void | Promise<void>` — called for each write.
### `BatchSink<T>`
- `constructor(batchSize, inner, name?)`
- Flushes to `inner.write(batch)` when buffer reaches `batchSize`
- `flush()` — sends remaining buffer to inner sink
## Implementation Notes
`BatchSink.close()` calls `flush()` first to ensure no records are lost when the pipeline closes. The inner sink receives `T[]` batches, not individual records — callers must implement the inner sink accordingly.

View File

@@ -0,0 +1,59 @@
# source
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides built-in `Source` implementations for the ETL pipeline. Sources are the extraction stage — they yield records via an `AsyncGenerator`. This module ships two general-purpose factories: `arraySource` for in-memory datasets and `generatorSource` for custom async generators.
## Features
- `arraySource(items, name?)` — wraps a plain array as a pipeline source; no I/O, zero latency
- `generatorSource(gen, name?)` — wraps an async generator factory for custom extraction logic
- Both implement the full `Source<T>` interface with `open`/`read`/`close` lifecycle
- Named for OTel span attribution
## Usage: Basic Example
```ts
import { arraySource } from "@suaveplan/etl";
const source = arraySource([{ id: "1", name: "Alice" }, { id: "2", name: "Bob" }]);
for await (const record of source.read()) {
console.log(record.name);
}
```
## Usage: Advanced Example
```ts
import { generatorSource } from "@suaveplan/etl";
const pagingSource = generatorSource(async function* () {
let cursor: string | undefined;
do {
const page = await fetchPage(cursor);
yield* page.records;
cursor = page.nextCursor;
} while (cursor);
}, "ApiPagingSource");
```
## API Reference
### `arraySource<T>(items, name?)`
- `items: T[]` — records to yield
- `name?: string` — source name for OTel (default: `"ArraySource"`)
- Returns `Source<T>`
### `generatorSource<T>(gen, name?)`
- `gen: () => AsyncGenerator<T>` — factory called each time `read()` is invoked
- `name?: string` — source name for OTel (default: `"GeneratorSource"`)
- Returns `Source<T>`
## Implementation Notes
Both factories produce no-op `open` and `close` methods. If your source requires real I/O lifecycle management (database cursors, file handles), implement the `Source<T>` interface directly rather than using these convenience factories.

View File

@@ -0,0 +1,60 @@
# transform
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides built-in `Transform` implementations for the ETL pipeline. Transforms are the middle stage that convert, filter, validate, or expand records. Each transform is a pure function wrapped in the `Transform<TIn, TOut>` interface. The `SKIP` sentinel enables filtering without throwing errors.
## Features
- `mapTransform(fn, name?)` — maps each record via `fn`; return `null` to skip the record
- `filterTransform(fn, name?)` — keeps records where `fn` returns `true`
- `validateTransform(schema, name?)` — parses with a Zod-compatible schema; throws `EtlError(ETL.VALIDATION_FAILED)` on failure
- `flatMapTransform(fn, name?)` — expands a record to multiple output records
- All transforms are async-capable and accept a `TransformContext` with index and abort signal
## Usage: Basic Example
```ts
import { mapTransform, filterTransform } from "@suaveplan/etl";
const toUpperCase = mapTransform((s: string) => s.toUpperCase());
const nonEmpty = filterTransform((s: string) => s.length > 0);
```
## Usage: Advanced Example
```ts
import { validateTransform } from "@suaveplan/etl";
import z from "zod";
const UserSchema = z.object({ id: z.string(), email: z.string().email() });
const validateUser = validateTransform(UserSchema, "ValidateUser");
// In a pipeline, invalid records throw EtlError(ETL.VALIDATION_FAILED)
```
## API Reference
### `mapTransform<TIn, TOut>(fn, name?)`
`fn: (record: TIn, ctx: TransformContext) => TOut | null | Promise<TOut | null>` — return `null` to skip.
### `filterTransform<T>(fn, name?)`
`fn: (record: T, ctx: TransformContext) => boolean | Promise<boolean>` — return `false` to skip.
### `validateTransform<T>(schema, name?)`
`schema: { parse: (data: unknown) => T }` — any Zod schema or compatible object.
### `flatMapTransform<TIn, TOut>(fn, name?)`
`fn: (record: TIn, ctx: TransformContext) => TOut[] | Promise<TOut[]>` — empty array skips.
## Implementation Notes
`validateTransform` re-throws validation failures as `EtlError` to integrate with the pipeline's `onError` callback. The underlying schema error is preserved as `cause`. The `SKIP` sentinel from `etl.types` is a unique symbol, avoiding confusion with falsy return values.

View File

@@ -0,0 +1,75 @@
# etl.types
## Purpose
Shared TypeScript type definitions for the `@suaveplan/etl` pipeline engine. Defines the core interfaces (`Source`, `Transform`, `Sink`), the `SKIP` sentinel, pipeline statistics, result types, and error handling contracts. All ETL implementations depend on these types, which are kept in a separate module to avoid circular imports.
## Features
- `Source<T>` — async generator source with `open`/`read`/`close` lifecycle
- `Transform<TIn, TOut>` — transforms a single record, returns `TOut | SKIP`
- `Sink<T>` — writes records with `open`/`write`/`flush`/`close` lifecycle
- `SKIP` unique symbol — filter records from the pipeline without throwing
- `TransformContext` — carries the record index and optional abort signal
- `PipelineStats` — counters for extracted, transformed, loaded, skipped records and duration
- `PipelineResult` — combines stats with a list of non-fatal errors
- `ErrorHandler` — per-record error callback returning `"skip" | "abort" | "retry"`
- `PipelineOptions<TIn>` — full configuration including batchSize, clock, bus
## Usage: Basic Example
```ts
import type { Source, Transform, Sink } from "@suaveplan/etl";
import { SKIP } from "@suaveplan/etl";
const myTransform: Transform<string, number> = {
name: "parse-int",
transform: (s) => {
const n = parseInt(s, 10);
return isNaN(n) ? SKIP : n;
},
};
```
## Usage: Advanced Example
```ts
import type { ErrorHandler, PipelineOptions } from "@suaveplan/etl";
const opts: PipelineOptions<string> = {
source: mySource,
batchSize: 500,
onError: (err, record, stage) => {
if (err.code === "ETL.VALIDATION_FAILED") return "skip";
if (stage === "source") return "abort";
return "retry";
},
};
```
## API Reference
### `Source<T>`
- `name: string` — display name for OTel spans
- `open(signal?): Promise<void>` — open connection
- `read(): AsyncGenerator<T>` — yield records
- `close(): Promise<void>` — release resources
### `Transform<TIn, TOut>`
- `name: string`
- `transform(record, ctx): Promise<TOut | SKIP> | TOut | SKIP`
### `Sink<T>`
- `name: string`
- `open(signal?)`, `write(record)`, `flush()`, `close()`
### `SKIP`
Unique symbol. Return from a transform to drop the record from the pipeline.
## Implementation Notes
The `SKIP` sentinel is a `unique symbol` rather than `null` or `undefined` to avoid ambiguity with valid `null` payloads. All lifecycle methods (`open`, `close`, `flush`) are async even when no real I/O occurs, to keep adapters interchangeable.