4.4 KiB
transform/pipeline — declarative transform pipeline
(no submodules)
Purpose
Implements a three-stage row-transform pipeline that any consumer can
drive without depending on the canonical ImportService: per-mapping
coerce + transform, computed fields, then field validation. The
pipeline is pure (no side effects, no I/O) and never throws — every
failure is captured as a TransformError and returned in the result
object, so callers can collect every failed row in one pass.
This module powers both the standalone transformRow / transformRows
helpers exported to consumers and the per-row inner loop of
ImportService.startImport. Keeping the pipeline separate from the
service means CLI tools, batch workers, and tests can drive the
transform logic against arbitrary input without spinning up an event
emitter or store adapter.
Features
coerceType— explicit-result coercion fornumber/boolean/datewith documented "valid" sets (1/0/yes/no/true/falsefor booleans, anythingDate.parse-able for dates).validateField— five built-in rules (required,min,max,pattern,custom);customis an opt-in extension point that always returnsundefinedso callers wire their own.transformRow— applies mappings, computed fields, and validations in order; pushesTransformErrorfor each failure and returns{ success, row, errors }.transformRows— array fan-out; errored rows are dropped fromrowsbut their errors are still collected inerrors.toRowError— bridge to the service-layerRowErrorshape so the service can re-emit transform failures on theimport.row_errorevent topic without duplicating shapes.
Usage: Basic Example
import { transformRow } from "@suaveplan/api-plugin-import";
const result = transformRow(
{ name: "Ada", age: "37" },
0,
{
mappings: [
{ sourceColumn: "name", targetField: "displayName" },
{ sourceColumn: "age", targetField: "age", type: "number" },
],
}
);
// result.row → { displayName: "Ada", age: 37 }
Usage: Advanced Example
import { transformRow } from "@suaveplan/api-plugin-import";
const result = transformRow(
{ first: "Ada", last: "Lovelace", born: "1815" },
0,
{
mappings: [
{ sourceColumn: "born", targetField: "birthYear", type: "number" },
],
computedFields: [
{
targetField: "fullName",
sourceColumns: ["first", "last"],
compute: (v) => `${String(v.first)} ${String(v.last)}`,
},
],
validations: [
{ field: "birthYear", rule: "min", param: 1800 },
],
}
);
// result.row → { birthYear: 1815, fullName: "Ada Lovelace" }
API Reference
coerceType(value: unknown, type: string): CoerceResultvalidateField(value, rule: FieldValidation, rowIndex): TransformError | undefinedtransformRow(source, rowIndex, config: TransformPipelineConfig): TransformRowResulttransformRows(sources, config): { rows; errors }toRowError(err: TransformError): RowErrorinterface TransformMapping { sourceColumn; targetField; type?; defaultValue?; required?; transform? }interface ComputedField { targetField; sourceColumns; compute }interface FieldValidation { field; rule; param?; message? }interface TransformError { row; field; message; value? }interface TransformRowResult { success; row; errors }interface TransformPipelineConfig { mappings; computedFields?; validations? }interface CoerceResult { ok; value }
Implementation Notes
- Coercion short-circuits on empty / null / undefined to
{ ok: true, value: undefined }so a downstreamdefaultValuecan still apply. - The transform function (
mapping.transform) is wrapped intryso a thrown library never escapes the pipeline; the row'ssuccessflag reports the failure and the rest of the mappings still process. customvalidation rules are intentionally no-op — extension is via thetransformcallback on the mapping, which has access to the raw value. Custom rules without a default are documented in the enterprise audit as "extended by consumers."validateFieldruns after both mapping and computed-field stages, so validations target the post-coercion result, not the raw source row.