Files
2026-05-27 23:29:53 +01:00

10 KiB

ETL completion

Package: @suaveplan/etl Status: PROPOSED Type: spec-conformance + feature

Why

The genesis port of @suaveplan/etl ships the core stage primitives (arraySource, mapTransform, BatchSink, etc.) and a Pipeline builder, but the surface around it is materially thinner than the upstream package at ../suaveplan/packages/universal/etl. Five concrete gaps make consumers either rebuild missing pieces in-app or abandon the package altogether:

  1. No PipelineContext — upstream exposes a mutable execution context (data, errors, stepResults, hooks, metadata K/V) that travels through every step. Genesis stages get isolated TransformContext snapshots, so cross-step state requires bolting on a separate map.
  2. No batch concurrency / timeout config — upstream's BatchConfig ships size, concurrency, and (via pipeline) per-batch timeouts. Genesis only honours size via BatchSink, so consumers can't opt into parallel batches or per-item timeout enforcement.
  3. No EtlHookMap lifecycle bus — upstream emits pipeline:start, pipeline:end, step:start, step:end, step:error, batch:start, batch:end, batch:error through a typed hook map. Genesis has no observability surface beyond raw error returns.
  4. Missing utility primitives — applyMappings, getNestedValue, setNestedValue, parallelMap, toBatches, withTimeout are all absent. These are the shared building blocks every non-trivial ETL pipeline needs and they belong in the package, not the call site.
  5. Missing FieldMapping type — upstream ships FieldMapping ({ from, to, transform? }) plus applyMappings(record, mappings). Genesis has no canonical mapping shape, so each consumer hand-rolls one.

This change closes the five gaps in one coordinated pass so downstream services (@suaveplan/packer import pipelines, future analytics ETLs) can rely on a single coherent ETL surface.

What changes

  • ADDED: core/context/context module — exports class PipelineContext and function createContext(options: PipelineContextOptions) returning a typed shared-state object with data, errors, stepResults, metadata map, hook registration, and an injectable LoggerLike / Clock / AbortSignal. The context threads through every step.
  • ADDED: BatchConfig interface — { size: number; concurrency?: number; perItemTimeoutMs?: number; batchTimeoutMs?: number; continueOnError?: boolean }. Pipeline config grows batch?: BatchConfig. The pipeline runner SHALL honour all four fields.
  • ADDED: types/etl-hooks module — exports EtlHookMap extending HookMap from @suaveplan/events::hooks.types, with keys "pipeline:started", "pipeline:completed", "stage:started", "stage:completed", "stage:failed", "batch:started", "batch:completed", "batch:failed". Plus EtlTransformHookData and EtlTransformingHandler aliases for the most common subscriber shape.
  • ADDED: utils/utils module — exports applyMappings(record, mappings), getNestedValue(obj, path), setNestedValue(obj, path, value), parallelMap(items, fn, concurrency), toBatches(items, size), withTimeout(promise, ms).
  • ADDED: FieldMapping type — { from: string; to: string; transform?: (value: unknown) => unknown }. mapTransform and any TransformConfig SHALL accept mappings?: readonly FieldMapping[] applied before the transform function.
  • MODIFIED: Pipeline and RunnablePipeline — accept and emit EtlHookMap events through an injectable HookBus (defaults to a fresh internal bus when omitted). Existing errorHandler API preserved.
  • ADDED: per-module sibling .md for every new file.

Out of scope

  • Connectors for specific databases (e.g. SurrealDB source, Postgres sink, S3 source) — separate adapter packages so each can pull in its native client and integration test fixture independently. This proposal only stabilises the runner contract.
  • CDC stream support — change-data-capture / event-source ingestion belongs to a streaming-etl change paired with @suaveplan/events interceptors; this proposal stays batch-pipeline-only.
  • Schema inference / auto-mapping — synthesising FieldMapping[] from a sample record is a separate research item.
  • Web Streams runner — the existing pipeline is in-memory and uses async iterators; a ReadableStream runner is its own change.
  • Distributed pipeline orchestration — multi-worker pipelines (queue-backed work items) require an @suaveplan/idempotency integration that isn't in scope here.

Source mapping

This change derives from the upstream package at ../suaveplan/packages/universal/etl/src and the upstream enterprise audit at ../suaveplan/enterprise/etl.md.

Requirement Source file Source lines Notes
PipelineContext class with mutable shared state packages/universal/etl/src/core/context/context.ts L1-L120 Verbatim port; thread through Clock / LoggerLike from @suaveplan/types per genesis convention
createContext(options) factory packages/universal/etl/src/core/context/context.ts (export at file end) Verbatim
BatchConfig interface (size, concurrency, continueOnError) packages/universal/etl/src/types/etl.types.ts L98-L106 Verbatim port; extend with perItemTimeoutMs and batchTimeoutMs (genesis additions, see "no source line" below)
EtlHookMap typed event bus packages/universal/etl/src/types/etl-hooks.ts L1-L29 Verbatim port; expand from one upstream key (etl:transforming) to the eight lifecycle keys upstream emits via PipelineEventPayload
EtlTransformHookData / EtlTransformingHandler packages/universal/etl/src/types/etl-hooks.ts L9-L29 Verbatim
applyMappings(record, mappings) utility packages/universal/etl/src/utils/utils.ts L48-L80 Verbatim
getNestedValue(obj, path) utility packages/universal/etl/src/utils/utils.ts L7-L23 Verbatim
setNestedValue(obj, path, value) utility packages/universal/etl/src/utils/utils.ts L29-L47 Verbatim
parallelMap(items, fn, concurrency) utility packages/universal/etl/src/utils/utils.ts L82-L120 Verbatim
toBatches(items, size) utility packages/universal/etl/src/utils/utils.ts L122-L140 Verbatim
withTimeout(promise, ms) utility packages/universal/etl/src/utils/utils.ts L142-L151 Verbatim
FieldMapping type packages/universal/etl/src/types/etl.types.ts L64-L72 Verbatim
pipeline:started / pipeline:completed / stage:* / batch:* lifecycle events packages/universal/etl/src/types/etl.types.ts L106-L130 Renamed from upstream pipeline:start / step:start etc. to lifecycle-named (started/completed/failed) per genesis convention; payload shapes preserved

Requirements with no source line

Requirement Justification
BatchConfig.perItemTimeoutMs and batchTimeoutMs fields Upstream models pipeline-wide timeoutMs only. Genesis enforces granular timeouts because @suaveplan/retry::withTimeout is already in the dep graph and per-item enforcement is a common production requirement (slow rows shouldn't stall the whole batch).
Hook keys renamed to lifecycle-named pattern (*:started / *:completed / *:failed) Genesis-wide naming convention applied across @suaveplan/migration, @suaveplan/network, @suaveplan/cache (see lifecycle keys in those packages). Upstream's *:start / *:end / *:error style was itself inconsistent across packages.
EtlHookMap built on HookMap from @suaveplan/events::hooks.types rather than upstream's bespoke listener registry Genesis-wide event bus convention (CLAUDE.md @suaveplan/events listing).
ETL_CODES registered idempotently via registerEtlErrorCodes() Genesis-wide error registry rule (CLAUDE.md §Workflow rule 2).

Verification approach

Each spec delta requirement carries a **Verified by:** line. The test files that will exist after this change ships:

  • packages/platform/etl/src/core/context/context.test.ts — set / get / delete metadata, hook register / off, errors / stepResults appended, AbortSignal propagation
  • packages/platform/etl/src/core/context/context.spec-conformance.test.ts — pins type-level PipelineContextOptions shape; injected Clock deterministic
  • packages/platform/etl/src/core/pipeline/pipeline.test.ts (extended) — BatchConfig honoured: size chunks input, concurrency runs N batches in parallel, perItemTimeoutMs rejects slow items with EtlError(BATCH_ITEM_TIMEOUT), batchTimeoutMs rejects slow batches, continueOnError skips failed batch
  • packages/platform/etl/src/types/etl-hooks/etl-hooks.test.ts — every key in EtlHookMap fires once per lifecycle transition with the correct payload
  • packages/platform/etl/src/utils/utils.test.ts — applyMappings / getNestedValue / setNestedValue / parallelMap / toBatches / withTimeout happy-path + edge cases (deep path missing, concurrency 0, batch size 0, timeout 0)
  • packages/platform/etl/src/utils/utils.spec-conformance.test.ts — parallelMap honours concurrency limit empirically; withTimeout rejects with EtlError(TIMEOUT)
  • packages/platform/etl/src/types/etl-hooks/etl-hooks.spec-conformance.test.ts — pins lifecycle key strings via expectTypeOf; pins payload shapes
  • packages/platform/etl/src/errors/etl-error.spec-conformance.test.ts — ETL_CODES registered after import; registerEtlErrorCodes idempotent

Impact

Affected dependents

Run bunx turbo ls --filter=...@suaveplan/etl to enumerate. Today grep -rl "@suaveplan/etl" packages/ --include="package.json" returns zero matches outside packages/platform/etl/package.json itself, so this change is purely additive.

  • @suaveplan/etl (self) — [x] tests run / [x] docs reviewed / [ ] no action needed
  • (no in-repo dependents) — verified by bunx turbo ls --filter=...@suaveplan/etl returning a single package.

If tests run is unchecked for any dependent, the change MUST NOT be folded into the canonical spec — finish that work first or split the proposal.

Migration impact

  • Pipeline consumers — no break. Existing constructors continue to work; PipelineContext, BatchConfig, and the hook bus are all opt-in additive.
  • mapTransform consumers — no break. New mappings?: readonly FieldMapping[] argument is optional.
  • PipelineOptions consumers — no break. New batch, hooks, and context fields are optional and default to no-op behavior.
  • SKIP consumers — no break. The SKIP symbol export is preserved.