convert-buddy-js 0.12.4 → 0.12.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +545 -4
- package/dist/browser.d.ts +1 -1
- package/dist/buddy-stream.js +16 -51
- package/dist/buddy-stream.js.map +1 -1
- package/dist/index.d.ts +1 -152
- package/dist/index.js +4 -21
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/tests/conversions/transform.test.js +1321 -0
- package/dist/tests/conversions/transform.test.js.map +1 -0
- package/dist/tests/stats/profiling.test.js +0 -22
- package/dist/tests/stats/profiling.test.js.map +1 -1
- package/dist/wasm/nodejs/convert_buddy_bg.wasm +0 -0
- package/dist/wasm/web/convert_buddy_bg.wasm +0 -0
- package/package.json +1 -1
- package/dist/unified-stream.js +0 -333
- package/dist/unified-stream.js.map +0 -1
package/README.md
CHANGED
|
@@ -8,6 +8,33 @@ A **high-performance, streaming-first** parser and converter for **CSV, XML, NDJ
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Install](#install)
|
|
14
|
+
- [Quick Start](#quick-start)
|
|
15
|
+
- [API Overview](#api-overview)
|
|
16
|
+
- [Simple API](#1-simple-api-for-small-files)
|
|
17
|
+
- [Streaming API](#2-streaming-api-for-large-files--real-time-processing)
|
|
18
|
+
- [Instance-based API](#3-instance-based-api)
|
|
19
|
+
- [Low-level API](#4-low-level-api-for-advanced-use-cases)
|
|
20
|
+
- [Stats & Monitoring](#stats--monitoring)
|
|
21
|
+
- [Format Detection & Structure Analysis](#format-detection--structure-analysis)
|
|
22
|
+
- [Real-World Examples](#real-world-examples)
|
|
23
|
+
- [Formats & Configuration](#formats--configuration)
|
|
24
|
+
- [Transformations & Field Mapping](#transformations--field-mapping)
|
|
25
|
+
- [Transform Basics](#transform-basics)
|
|
26
|
+
- [Transform Modes](#transform-modes)
|
|
27
|
+
- [Field Mapping](#field-mapping)
|
|
28
|
+
- [Compute Functions](#compute-functions)
|
|
29
|
+
- [Type Coercion](#type-coercion)
|
|
30
|
+
- [Error Handling](#transform-error-handling)
|
|
31
|
+
- [Transform Examples](#transform-examples)
|
|
32
|
+
- [How it Works](#how-it-works)
|
|
33
|
+
- [Benchmarks](#benchmarks-repository)
|
|
34
|
+
- [License](#license)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
11
38
|
## Status & Quality
|
|
12
39
|
|
|
13
40
|
[](https://snyk.io/test/github/brunohanss/convert-buddy)
|
|
@@ -678,16 +705,26 @@ Supported
|
|
|
678
705
|
|
|
679
706
|
## Transformations & Field Mapping
|
|
680
707
|
|
|
681
|
-
Convert Buddy
|
|
708
|
+
Convert Buddy includes a **powerful Rust-based transform engine** that enables field-level transformations, computed fields, type coercion, and data reshaping during conversion — all executed in WASM for maximum performance with zero JavaScript overhead.
|
|
709
|
+
|
|
710
|
+
### Transform Basics
|
|
711
|
+
|
|
712
|
+
Apply transforms using the `transform` option in any conversion:
|
|
682
713
|
|
|
683
714
|
```ts
|
|
684
|
-
|
|
715
|
+
import { ConvertBuddy } from "convert-buddy-js";
|
|
716
|
+
|
|
717
|
+
const buddy = new ConvertBuddy();
|
|
718
|
+
|
|
719
|
+
const result = await buddy.convert(csvData, {
|
|
685
720
|
outputFormat: "json",
|
|
686
721
|
transform: {
|
|
687
|
-
mode: "
|
|
722
|
+
mode: "replace",
|
|
688
723
|
fields: [
|
|
724
|
+
{ targetFieldName: "id", required: true },
|
|
689
725
|
{ targetFieldName: "full_name", compute: "concat(first, ' ', last)" },
|
|
690
726
|
{ targetFieldName: "age", coerce: { type: "i64" }, defaultValue: 0 },
|
|
727
|
+
{ targetFieldName: "email", compute: "lower(trim(email))" },
|
|
691
728
|
],
|
|
692
729
|
onMissingField: "null",
|
|
693
730
|
onCoerceError: "null",
|
|
@@ -695,7 +732,511 @@ const out = await buddy.convert(csvString, {
|
|
|
695
732
|
});
|
|
696
733
|
```
|
|
697
734
|
|
|
698
|
-
|
|
735
|
+
**Key Features:**
|
|
736
|
+
- 🚀 **WASM performance** — all transforms execute in Rust/WASM
|
|
737
|
+
- 🔄 **Streaming** — transforms work on line-by-line records without buffering
|
|
738
|
+
- 🧮 **30+ compute functions** — string manipulation, math, logic, type conversions
|
|
739
|
+
- 🎯 **Type coercion** — convert strings to numbers, booleans, timestamps
|
|
740
|
+
- ⚡ **Zero memory overhead** — no intermediate objects or copies in JS
|
|
741
|
+
- 🛡️ **Error policies** — control behavior for missing fields and coercion failures
|
|
742
|
+
|
|
743
|
+
### Transform Modes
|
|
744
|
+
|
|
745
|
+
**`mode: "replace"`** (default)
|
|
746
|
+
Outputs only the fields you explicitly map. Original fields are discarded.
|
|
747
|
+
|
|
748
|
+
```ts
|
|
749
|
+
// Input: { "first": "Alice", "last": "Smith", "age": 30, "city": "NYC" }
|
|
750
|
+
// Output: { "full_name": "Alice Smith", "age": 30 }
|
|
751
|
+
|
|
752
|
+
transform: {
|
|
753
|
+
mode: "replace",
|
|
754
|
+
fields: [
|
|
755
|
+
{ targetFieldName: "full_name", compute: "concat(first, ' ', last)" },
|
|
756
|
+
{ targetFieldName: "age" },
|
|
757
|
+
],
|
|
758
|
+
}
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
**`mode: "augment"`**
|
|
762
|
+
Preserves all original fields and adds/overwrites mapped fields.
|
|
763
|
+
|
|
764
|
+
```ts
|
|
765
|
+
// Input: { "name": "Alice", "age": 30 }
|
|
766
|
+
// Output: { "name": "Alice", "age": 30, "name_upper": "ALICE", "is_adult": true }
|
|
767
|
+
|
|
768
|
+
transform: {
|
|
769
|
+
mode: "augment",
|
|
770
|
+
fields: [
|
|
771
|
+
{ targetFieldName: "name_upper", compute: "upper(name)" },
|
|
772
|
+
{ targetFieldName: "is_adult", compute: "gte(age, 18)" },
|
|
773
|
+
],
|
|
774
|
+
}
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
### Field Mapping
|
|
778
|
+
|
|
779
|
+
Each field in the `fields` array defines a mapping:
|
|
780
|
+
|
|
781
|
+
```ts
|
|
782
|
+
type FieldMap = {
|
|
783
|
+
targetFieldName: string; // Output field name
|
|
784
|
+
originFieldName?: string; // Input field name (defaults to targetFieldName)
|
|
785
|
+
required?: boolean; // Fail if missing
|
|
786
|
+
defaultValue?: any; // Fallback value
|
|
787
|
+
coerce?: Coerce; // Type conversion
|
|
788
|
+
compute?: string; // Expression to compute value
|
|
789
|
+
};
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
**Basic field pass-through:**
|
|
793
|
+
```ts
|
|
794
|
+
{ targetFieldName: "name" } // Maps input "name" to output "name"
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
**Rename fields:**
|
|
798
|
+
```ts
|
|
799
|
+
{ targetFieldName: "fullName", originFieldName: "full_name" } // Rename full_name → fullName
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
**Required fields:**
|
|
803
|
+
```ts
|
|
804
|
+
{ targetFieldName: "id", required: true } // Fail conversion if "id" is missing
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
**Default values:**
|
|
808
|
+
```ts
|
|
809
|
+
{ targetFieldName: "status", defaultValue: "active" } // Use "active" if field is missing or null
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
**Computed fields:**
|
|
813
|
+
```ts
|
|
814
|
+
{ targetFieldName: "display", compute: "concat(first, ' ', last)" }
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
**Type coercion:**
|
|
818
|
+
```ts
|
|
819
|
+
{ targetFieldName: "age", coerce: { type: "i64" } } // Convert string "30" → number 30
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
**Combined:**
|
|
823
|
+
```ts
|
|
824
|
+
{
|
|
825
|
+
targetFieldName: "age_years",
|
|
826
|
+
originFieldName: "age",
|
|
827
|
+
coerce: { type: "i64" },
|
|
828
|
+
defaultValue: 0,
|
|
829
|
+
required: false,
|
|
830
|
+
}
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
### Compute Functions
|
|
834
|
+
|
|
835
|
+
Transform expressions support **30+ built-in functions** for data manipulation:
|
|
836
|
+
|
|
837
|
+
#### String Functions
|
|
838
|
+
|
|
839
|
+
| Function | Description | Example |
|
|
840
|
+
|----------|-------------|---------|
|
|
841
|
+
| `concat(...)` | Join strings | `concat(first, " ", last)` → `"Alice Smith"` |
|
|
842
|
+
| `lower(s)` | Lowercase | `lower("HELLO")` → `"hello"` |
|
|
843
|
+
| `upper(s)` | Uppercase | `upper("hello")` → `"HELLO"` |
|
|
844
|
+
| `trim(s)` | Remove whitespace | `trim(" text ")` → `"text"` |
|
|
845
|
+
| `trim_start(s)` | Remove leading whitespace | `trim_start(" text")` → `"text"` |
|
|
846
|
+
| `trim_end(s)` | Remove trailing whitespace | `trim_end("text ")` → `"text"` |
|
|
847
|
+
| `substring(s, start, end)` | Extract substring | `substring("hello", 0, 3)` → `"hel"` |
|
|
848
|
+
| `replace(s, old, new)` | Replace text | `replace("foo bar", "foo", "baz")` → `"baz bar"` |
|
|
849
|
+
| `len(s)` | String length | `len("hello")` → `5` |
|
|
850
|
+
| `starts_with(s, prefix)` | Check prefix | `starts_with("https://...", "https")` → `true` |
|
|
851
|
+
| `ends_with(s, suffix)` | Check suffix | `ends_with("file.pdf", ".pdf")` → `true` |
|
|
852
|
+
| `contains(s, substr)` | Check substring | `contains("hello world", "world")` → `true` |
|
|
853
|
+
| `pad_start(s, len, char)` | Left-pad | `pad_start("42", 5, "0")` → `"00042"` |
|
|
854
|
+
| `pad_end(s, len, char)` | Right-pad | `pad_end("x", 3, "_")` → `"x__"` |
|
|
855
|
+
| `repeat(s, n)` | Repeat string | `repeat("ab", 3)` → `"ababab"` |
|
|
856
|
+
| `reverse(s)` | Reverse string | `reverse("hello")` → `"olleh"` |
|
|
857
|
+
| `split(s, delim)` | Split to array | `split("a,b,c", ",")` → `["a","b","c"]` |
|
|
858
|
+
| `join(arr, delim)` | Join array | `join(["a","b"], ",")` → `"a,b"` |
|
|
859
|
+
|
|
860
|
+
#### Math Functions
|
|
861
|
+
|
|
862
|
+
| Function | Description | Example |
|
|
863
|
+
|----------|-------------|---------|
|
|
864
|
+
| `+`, `-`, `*`, `/` | Arithmetic operators | `price * 1.1` |
|
|
865
|
+
| `round(n)` | Round to nearest integer | `round(3.7)` → `4` |
|
|
866
|
+
| `floor(n)` | Round down | `floor(3.9)` → `3` |
|
|
867
|
+
| `ceil(n)` | Round up | `ceil(3.1)` → `4` |
|
|
868
|
+
| `abs(n)` | Absolute value | `abs(-5)` → `5` |
|
|
869
|
+
| `min(...)` | Minimum value | `min(a, b, c)` → smallest |
|
|
870
|
+
| `max(...)` | Maximum value | `max(a, b, c)` → largest |
|
|
871
|
+
|
|
872
|
+
#### Logic & Comparison
|
|
873
|
+
|
|
874
|
+
| Function | Description | Example |
|
|
875
|
+
|----------|-------------|---------|
|
|
876
|
+
| `if(cond, true_val, false_val)` | Conditional | `if(age >= 18, "adult", "minor")` |
|
|
877
|
+
| `not(bool)` | Boolean NOT | `not(active)` → opposite |
|
|
878
|
+
| `eq(a, b)` | Equals | `eq(status, "active")` → `true/false` |
|
|
879
|
+
| `ne(a, b)` | Not equals | `ne(a, b)` |
|
|
880
|
+
| `gt(a, b)` | Greater than | `gt(age, 18)` |
|
|
881
|
+
| `gte(a, b)` | Greater than or equal | `gte(age, 18)` |
|
|
882
|
+
| `lt(a, b)` | Less than | `lt(price, 100)` |
|
|
883
|
+
| `lte(a, b)` | Less than or equal | `lte(score, 50)` |
|
|
884
|
+
|
|
885
|
+
#### Type Checking
|
|
886
|
+
|
|
887
|
+
| Function | Description | Example |
|
|
888
|
+
|----------|-------------|---------|
|
|
889
|
+
| `is_null(val)` | Check if null | `is_null(optional_field)` → `true/false` |
|
|
890
|
+
| `is_number(val)` | Check if number | `is_number(val)` → `true/false` |
|
|
891
|
+
| `is_string(val)` | Check if string | `is_string(val)` → `true/false` |
|
|
892
|
+
| `is_bool(val)` | Check if boolean | `is_bool(val)` → `true/false` |
|
|
893
|
+
|
|
894
|
+
#### Type Conversion
|
|
895
|
+
|
|
896
|
+
| Function | Description | Example |
|
|
897
|
+
|----------|-------------|---------|
|
|
898
|
+
| `to_string(val)` | Convert to string | `to_string(42)` → `"42"` |
|
|
899
|
+
| `parse_int(s)` | Parse string to integer | `parse_int("123")` → `123` |
|
|
900
|
+
| `parse_float(s)` | Parse string to float | `parse_float("3.14")` → `3.14` |
|
|
901
|
+
|
|
902
|
+
#### Utility Functions
|
|
903
|
+
|
|
904
|
+
| Function | Description | Example |
|
|
905
|
+
|----------|-------------|---------|
|
|
906
|
+
| `coalesce(...)` | First non-null value | `coalesce(alt1, alt2, "default")` |
|
|
907
|
+
| `default(val, fallback)` | Fallback for null | `default(optional, "N/A")` |
|
|
908
|
+
|
|
909
|
+
**Nested expressions:**
|
|
910
|
+
```ts
|
|
911
|
+
compute: "upper(trim(concat(first, ' ', last)))"
|
|
912
|
+
// Input: { first: " alice ", last: " smith " }
|
|
913
|
+
// Output: "ALICE SMITH"
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
**Complex logic:**
|
|
917
|
+
```ts
|
|
918
|
+
compute: "if(gte(age, 18), 'adult', if(gte(age, 13), 'teen', 'child'))"
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
### Type Coercion
|
|
922
|
+
|
|
923
|
+
Convert field types automatically:
|
|
924
|
+
|
|
925
|
+
```ts
|
|
926
|
+
type Coerce =
|
|
927
|
+
| { type: "string" }
|
|
928
|
+
| { type: "i64" } // 64-bit integer
|
|
929
|
+
| { type: "f64" } // 64-bit float
|
|
930
|
+
| { type: "bool" }
|
|
931
|
+
| { type: "timestamp_ms"; format?: "iso8601" | "unix_ms" | "unix_s" };
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
**String coercion:**
|
|
935
|
+
```ts
|
|
936
|
+
// Number → String: 42 → "42"
|
|
937
|
+
// Boolean → String: true → "true"
|
|
938
|
+
// Null → String: null → ""
|
|
939
|
+
{ targetFieldName: "age_str", originFieldName: "age", coerce: { type: "string" } }
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
**Integer coercion (`i64`):**
|
|
943
|
+
```ts
|
|
944
|
+
// String → Integer: "42" → 42
|
|
945
|
+
// Float → Integer: 3.7 → 3 (truncates)
|
|
946
|
+
// Boolean → Integer: true → 1, false → 0
|
|
947
|
+
{ targetFieldName: "count", coerce: { type: "i64" } }
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
**Float coercion (`f64`):**
|
|
951
|
+
```ts
|
|
952
|
+
// String → Float: "3.14" → 3.14
|
|
953
|
+
// Integer → Float: 42 → 42.0
|
|
954
|
+
{ targetFieldName: "price", coerce: { type: "f64" } }
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
**Boolean coercion:**
|
|
958
|
+
```ts
|
|
959
|
+
// String → Boolean: "true"/"false" → true/false, "1"/"0" → true/false
|
|
960
|
+
// Number → Boolean: 0 → false, non-zero → true
|
|
961
|
+
{ targetFieldName: "active", coerce: { type: "bool" } }
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
**Timestamp coercion:**
|
|
965
|
+
```ts
|
|
966
|
+
// ISO8601 string → Unix timestamp (ms)
|
|
967
|
+
{
|
|
968
|
+
targetFieldName: "created_ts",
|
|
969
|
+
originFieldName: "created_at",
|
|
970
|
+
coerce: { type: "timestamp_ms", format: "iso8601" }
|
|
971
|
+
}
|
|
972
|
+
// "2024-01-15T10:30:00Z" → 1705315800000
|
|
973
|
+
|
|
974
|
+
// Unix seconds → milliseconds
|
|
975
|
+
{
|
|
976
|
+
targetFieldName: "updated_ts",
|
|
977
|
+
coerce: { type: "timestamp_ms", format: "unix_s" }
|
|
978
|
+
}
|
|
979
|
+
// 1705315800 → 1705315800000
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
### Transform Error Handling
|
|
983
|
+
|
|
984
|
+
Control behavior when fields are missing or coercion fails:
|
|
985
|
+
|
|
986
|
+
**`onMissingField`** — What to do when a field is missing:
|
|
987
|
+
- `"error"` — Fail the entire conversion
|
|
988
|
+
- `"null"` — Insert `null` for missing fields
|
|
989
|
+
- `"drop"` — Omit the field from output
|
|
990
|
+
|
|
991
|
+
```ts
|
|
992
|
+
transform: {
|
|
993
|
+
fields: [{ targetFieldName: "optional_field" }],
|
|
994
|
+
onMissingField: "null", // Missing fields become null
|
|
995
|
+
}
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
**`onMissingRequired`** — What to do when a required field is missing:
|
|
999
|
+
- `"error"` — Fail the conversion (default)
|
|
1000
|
+
- `"abort"` — Stop processing this record
|
|
1001
|
+
|
|
1002
|
+
```ts
|
|
1003
|
+
transform: {
|
|
1004
|
+
fields: [{ targetFieldName: "id", required: true }],
|
|
1005
|
+
onMissingRequired: "error", // Fail if "id" is missing
|
|
1006
|
+
}
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
**`onCoerceError`** — What to do when type coercion fails:
|
|
1010
|
+
- `"error"` — Fail the entire conversion
|
|
1011
|
+
- `"null"` — Set field to `null` on coercion failure
|
|
1012
|
+
- `"dropRecord"` — Silently skip records that fail coercion
|
|
1013
|
+
|
|
1014
|
+
```ts
|
|
1015
|
+
transform: {
|
|
1016
|
+
fields: [{ targetFieldName: "age", coerce: { type: "i64" } }],
|
|
1017
|
+
onCoerceError: "null", // "invalid" → null instead of throwing
|
|
1018
|
+
}
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
**Skip invalid records:**
|
|
1022
|
+
```ts
|
|
1023
|
+
// Drop records where age cannot be parsed as integer
|
|
1024
|
+
transform: {
|
|
1025
|
+
fields: [
|
|
1026
|
+
{ targetFieldName: "name" },
|
|
1027
|
+
{ targetFieldName: "age", coerce: { type: "i64" } },
|
|
1028
|
+
],
|
|
1029
|
+
onCoerceError: "dropRecord", // Skip bad records silently
|
|
1030
|
+
}
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
### Transform Examples
|
|
1034
|
+
|
|
1035
|
+
**Example 1: Clean and normalize user data**
|
|
1036
|
+
|
|
1037
|
+
```ts
|
|
1038
|
+
const controller = buddy.stream("users.csv", {
|
|
1039
|
+
outputFormat: "json",
|
|
1040
|
+
transform: {
|
|
1041
|
+
mode: "replace",
|
|
1042
|
+
fields: [
|
|
1043
|
+
// Required ID
|
|
1044
|
+
{ targetFieldName: "id", required: true },
|
|
1045
|
+
|
|
1046
|
+
// Normalize name
|
|
1047
|
+
{
|
|
1048
|
+
targetFieldName: "full_name",
|
|
1049
|
+
compute: "trim(concat(first_name, ' ', last_name))"
|
|
1050
|
+
},
|
|
1051
|
+
|
|
1052
|
+
// Lowercase email
|
|
1053
|
+
{ targetFieldName: "email", compute: "lower(trim(email))" },
|
|
1054
|
+
|
|
1055
|
+
// Parse age as integer with default
|
|
1056
|
+
{
|
|
1057
|
+
targetFieldName: "age",
|
|
1058
|
+
coerce: { type: "i64" },
|
|
1059
|
+
defaultValue: 0
|
|
1060
|
+
},
|
|
1061
|
+
|
|
1062
|
+
// Active status as boolean
|
|
1063
|
+
{ targetFieldName: "active", coerce: { type: "bool" } },
|
|
1064
|
+
],
|
|
1065
|
+
onMissingField: "null",
|
|
1066
|
+
onCoerceError: "null",
|
|
1067
|
+
},
|
|
1068
|
+
onRecords: async (ctrl, records) => {
|
|
1069
|
+
await db.users.insertMany(records);
|
|
1070
|
+
},
|
|
1071
|
+
});
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
**Example 2: Calculate derived fields**
|
|
1075
|
+
|
|
1076
|
+
```ts
|
|
1077
|
+
// Add computed fields to existing data
|
|
1078
|
+
transform: {
|
|
1079
|
+
mode: "augment", // Keep all original fields
|
|
1080
|
+
fields: [
|
|
1081
|
+
// Calculate total with tax
|
|
1082
|
+
{
|
|
1083
|
+
targetFieldName: "total_with_tax",
|
|
1084
|
+
compute: "round(price * (1 + tax_rate))"
|
|
1085
|
+
},
|
|
1086
|
+
|
|
1087
|
+
// Discount percentage
|
|
1088
|
+
{
|
|
1089
|
+
targetFieldName: "discount_pct",
|
|
1090
|
+
compute: "round((original_price - price) / original_price * 100)"
|
|
1091
|
+
},
|
|
1092
|
+
|
|
1093
|
+
// Is on sale?
|
|
1094
|
+
{
|
|
1095
|
+
targetFieldName: "on_sale",
|
|
1096
|
+
compute: "lt(price, original_price)"
|
|
1097
|
+
},
|
|
1098
|
+
],
|
|
1099
|
+
}
|
|
1100
|
+
```
|
|
1101
|
+
|
|
1102
|
+
**Example 3: Data validation and filtering**
|
|
1103
|
+
|
|
1104
|
+
```ts
|
|
1105
|
+
// Only keep records with valid email and age >= 18
|
|
1106
|
+
const validRecords = [];
|
|
1107
|
+
|
|
1108
|
+
const controller = buddy.stream(csvData, {
|
|
1109
|
+
outputFormat: "ndjson",
|
|
1110
|
+
transform: {
|
|
1111
|
+
mode: "replace",
|
|
1112
|
+
fields: [
|
|
1113
|
+
{ targetFieldName: "email", required: true },
|
|
1114
|
+
{ targetFieldName: "age", coerce: { type: "i64" }, required: true },
|
|
1115
|
+
{
|
|
1116
|
+
targetFieldName: "is_adult",
|
|
1117
|
+
compute: "gte(age, 18)"
|
|
1118
|
+
},
|
|
1119
|
+
],
|
|
1120
|
+
onMissingRequired: "error",
|
|
1121
|
+
onCoerceError: "dropRecord", // Skip records with invalid age
|
|
1122
|
+
},
|
|
1123
|
+
onRecords: (ctrl, records) => {
|
|
1124
|
+
// Filter for adults only
|
|
1125
|
+
const adults = records.filter(r => r.is_adult);
|
|
1126
|
+
validRecords.push(...adults);
|
|
1127
|
+
},
|
|
1128
|
+
});
|
|
1129
|
+
```
|
|
1130
|
+
|
|
1131
|
+
**Example 4: Format conversion with field mapping**
|
|
1132
|
+
|
|
1133
|
+
```ts
|
|
1134
|
+
// Convert XML to CSV with field restructuring
|
|
1135
|
+
const controller = buddy.stream("data.xml", {
|
|
1136
|
+
inputFormat: "xml",
|
|
1137
|
+
outputFormat: "csv",
|
|
1138
|
+
xmlConfig: { recordElement: "user" },
|
|
1139
|
+
transform: {
|
|
1140
|
+
mode: "replace",
|
|
1141
|
+
fields: [
|
|
1142
|
+
{ targetFieldName: "user_id", originFieldName: "id" },
|
|
1143
|
+
{ targetFieldName: "name", compute: "concat(firstName, ' ', lastName)" },
|
|
1144
|
+
{ targetFieldName: "email_domain", compute: "split(email, '@')[1]" },
|
|
1145
|
+
{
|
|
1146
|
+
targetFieldName: "created_date",
|
|
1147
|
+
originFieldName: "createdAt",
|
|
1148
|
+
coerce: { type: "timestamp_ms", format: "iso8601" }
|
|
1149
|
+
},
|
|
1150
|
+
],
|
|
1151
|
+
},
|
|
1152
|
+
onData: (chunk) => {
|
|
1153
|
+
fs.appendFileSync("output.csv", chunk);
|
|
1154
|
+
},
|
|
1155
|
+
});
|
|
1156
|
+
```
|
|
1157
|
+
|
|
1158
|
+
**Example 5: Streaming with real-time transforms**
|
|
1159
|
+
|
|
1160
|
+
```ts
|
|
1161
|
+
// Process large file with transforms in streaming mode
|
|
1162
|
+
let processedCount = 0;
|
|
1163
|
+
|
|
1164
|
+
const controller = buddy.stream("massive-dataset.csv", {
|
|
1165
|
+
outputFormat: "ndjson",
|
|
1166
|
+
recordBatchSize: 1000,
|
|
1167
|
+
transform: {
|
|
1168
|
+
mode: "replace",
|
|
1169
|
+
fields: [
|
|
1170
|
+
{ targetFieldName: "id" },
|
|
1171
|
+
{ targetFieldName: "name_upper", compute: "upper(name)" },
|
|
1172
|
+
{ targetFieldName: "price", coerce: { type: "f64" } },
|
|
1173
|
+
{
|
|
1174
|
+
targetFieldName: "price_category",
|
|
1175
|
+
compute: "if(gt(price, 100), 'premium', if(gt(price, 50), 'mid', 'budget'))"
|
|
1176
|
+
},
|
|
1177
|
+
],
|
|
1178
|
+
onMissingField: "drop",
|
|
1179
|
+
},
|
|
1180
|
+
onRecords: async (ctrl, records, stats) => {
|
|
1181
|
+
processedCount += records.length;
|
|
1182
|
+
console.log(`Transformed ${processedCount} records at ${stats.throughputMbPerSec.toFixed(2)} MB/s`);
|
|
1183
|
+
|
|
1184
|
+
// Stream directly to database
|
|
1185
|
+
await db.products.insertMany(records);
|
|
1186
|
+
},
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
await controller.done;
|
|
1190
|
+
```
|
|
1191
|
+
|
|
1192
|
+
**Example 6: Complex field transformations**
|
|
1193
|
+
|
|
1194
|
+
```ts
|
|
1195
|
+
transform: {
|
|
1196
|
+
mode: "replace",
|
|
1197
|
+
fields: [
|
|
1198
|
+
// Combine multiple fields with formatting
|
|
1199
|
+
{
|
|
1200
|
+
targetFieldName: "address",
|
|
1201
|
+
compute: "concat(street, ', ', city, ', ', state, ' ', zip)"
|
|
1202
|
+
},
|
|
1203
|
+
|
|
1204
|
+
// Conditional logic
|
|
1205
|
+
{
|
|
1206
|
+
targetFieldName: "shipping_fee",
|
|
1207
|
+
compute: "if(eq(country, 'US'), 5.00, if(eq(country, 'CA'), 7.50, 12.00))"
|
|
1208
|
+
},
|
|
1209
|
+
|
|
1210
|
+
// Nested string operations
|
|
1211
|
+
{
|
|
1212
|
+
targetFieldName: "username",
|
|
1213
|
+
compute: "lower(trim(replace(email, '@', '_at_')))"
|
|
1214
|
+
},
|
|
1215
|
+
|
|
1216
|
+
// Math with multiple fields
|
|
1217
|
+
{
|
|
1218
|
+
targetFieldName: "bmi",
|
|
1219
|
+
compute: "round(weight / ((height / 100) * (height / 100)))"
|
|
1220
|
+
},
|
|
1221
|
+
|
|
1222
|
+
// String manipulation chain
|
|
1223
|
+
{
|
|
1224
|
+
targetFieldName: "display_name",
|
|
1225
|
+
compute: "concat(upper(substring(first, 0, 1)), '. ', last)"
|
|
1226
|
+
},
|
|
1227
|
+
],
|
|
1228
|
+
}
|
|
1229
|
+
```
|
|
1230
|
+
|
|
1231
|
+
### Performance Notes
|
|
1232
|
+
|
|
1233
|
+
- ✅ **Zero JS overhead** — transforms execute entirely in Rust/WASM
|
|
1234
|
+
- ✅ **Streaming** — processes line-by-line, no buffering of entire dataset
|
|
1235
|
+
- ✅ **Type safety** — expression parser validates syntax at compile time
|
|
1236
|
+
- ✅ **Memory efficient** — uses WASM linear memory, no JS objects created
|
|
1237
|
+
- ⚡ **Throughput** — typically adds <10% overhead vs. raw conversion
|
|
1238
|
+
|
|
1239
|
+
For extremely complex transformations (e.g., API lookups, database joins), consider using `onRecords` callback for post-processing in JavaScript.
|
|
699
1240
|
|
|
700
1241
|
---
|
|
701
1242
|
|
package/dist/browser.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ConvertOptions, Format } from './index.js';
|
|
2
|
-
export { BuddyController,
|
|
2
|
+
export { BuddyController, BuddyStats, BuddyStreamOptions, Coerce, ConvertBuddy, ConvertBuddyOptions, ConvertBuddyTransformStream, ConvertBuddy as Converter, CsvConfig, CsvDetection, DetectInput, DetectOptions, FieldMap, JsonDetection, NdjsonDetection, ProgressCallback, Stats, StructureDetection, TransformConfig, TransformMode, XmlConfig, XmlDetection, autoDetectConfig, convertAny, convertAnyToString, detectCsvFieldsAndDelimiter, detectFormat, detectStructure, detectXmlElements, getExtension, getMimeType, getOptimalThreadCount, getSuggestedFilename, getThreadingInfo, isWasmThreadingSupported, stream } from './index.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Browser entry point for convert-buddy-js
|