@semilayer/data-mapping 1.1.0

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.
@@ -0,0 +1,23 @@
1
+ import { FieldConfig, TransformSpec, BuiltinTransform } from '@semilayer/core';
2
+
3
+ interface ApplyFieldMappingOptions {
4
+ nullValues?: unknown[];
5
+ }
6
+ /**
7
+ * Applies field definitions to a raw source row, producing the output row.
8
+ * Each field resolves its source value (from `from` or identity), applies
9
+ * merge/transform/null handling, and writes to the output under the field key.
10
+ */
11
+ declare function applyFieldMapping(row: Record<string, unknown>, fields: Record<string, FieldConfig>, opts?: ApplyFieldMappingOptions): Record<string, unknown>;
12
+
13
+ /**
14
+ * Execute a single transform on a value.
15
+ * Never throws — data massaging, not validation.
16
+ */
17
+ declare function executeTransform(value: unknown, transform: BuiltinTransform, field?: string, row?: Record<string, unknown>): unknown;
18
+ /**
19
+ * Execute a transform spec (single or chain) on a value.
20
+ */
21
+ declare function applyTransforms(value: unknown, spec: TransformSpec, field?: string, row?: Record<string, unknown>): unknown;
22
+
23
+ export { type ApplyFieldMappingOptions, applyFieldMapping, applyTransforms, executeTransform };
package/dist/index.js ADDED
@@ -0,0 +1,132 @@
1
+ // src/transforms.ts
2
+ function executeTransform(value, transform, field, row) {
3
+ try {
4
+ switch (transform.type) {
5
+ case "toString":
6
+ return value == null ? "" : String(value);
7
+ case "toNumber": {
8
+ if (value == null) return NaN;
9
+ const n = Number(value);
10
+ return n;
11
+ }
12
+ case "toBoolean": {
13
+ if (value == null) return false;
14
+ if (typeof value === "boolean") return value;
15
+ if (typeof value === "number") return value !== 0;
16
+ const s = String(value).toLowerCase().trim();
17
+ return s === "true" || s === "1" || s === "yes";
18
+ }
19
+ case "toDate": {
20
+ if (value == null) return null;
21
+ const d = new Date(value);
22
+ return isNaN(d.getTime()) ? null : d.toISOString();
23
+ }
24
+ case "round": {
25
+ const num = typeof value === "number" ? value : Number(value);
26
+ if (isNaN(num)) return value;
27
+ const decimals = transform.decimals ?? 0;
28
+ const factor = Math.pow(10, decimals);
29
+ const mode = transform.mode ?? "round";
30
+ return Math[mode](num * factor) / factor;
31
+ }
32
+ case "trim":
33
+ return value == null ? "" : String(value).trim();
34
+ case "lowercase":
35
+ return value == null ? "" : String(value).toLowerCase();
36
+ case "uppercase":
37
+ return value == null ? "" : String(value).toUpperCase();
38
+ case "default":
39
+ return value == null ? transform.value : value;
40
+ case "split": {
41
+ if (value == null) return [];
42
+ return String(value).split(transform.separator);
43
+ }
44
+ case "join": {
45
+ if (Array.isArray(value)) return value.join(transform.separator);
46
+ return value == null ? "" : String(value);
47
+ }
48
+ case "truncate": {
49
+ if (value == null) return "";
50
+ return String(value).slice(0, transform.length);
51
+ }
52
+ case "replace": {
53
+ if (value == null) return "";
54
+ return String(value).replace(new RegExp(transform.pattern, "g"), transform.replacement);
55
+ }
56
+ case "custom": {
57
+ const fn = new Function("value", "field", "row", transform.body);
58
+ return fn(value, field, row);
59
+ }
60
+ default:
61
+ return value;
62
+ }
63
+ } catch {
64
+ return value;
65
+ }
66
+ }
67
+ function applyTransforms(value, spec, field, row) {
68
+ const transforms = Array.isArray(spec) ? spec : [spec];
69
+ let result = value;
70
+ for (const t of transforms) {
71
+ result = executeTransform(result, t, field, row);
72
+ }
73
+ return result;
74
+ }
75
+
76
+ // src/apply.ts
77
+ function applyFieldMapping(row, fields, opts) {
78
+ if (opts?.nullValues && opts.nullValues.length > 0) {
79
+ replaceNullSentinels(row, opts.nullValues);
80
+ }
81
+ const output = {};
82
+ for (const [outputName, field] of Object.entries(fields)) {
83
+ let value;
84
+ if (field.from === void 0) {
85
+ value = row[outputName];
86
+ } else if (Array.isArray(field.from)) {
87
+ if (field.merge === "coalesce") {
88
+ value = field.from.reduce((acc, src) => acc ?? row[src], void 0);
89
+ } else {
90
+ const parts = field.from.map((src) => row[src]).filter((v) => v != null);
91
+ value = parts.map(String).join(field.separator ?? " ");
92
+ }
93
+ } else {
94
+ value = row[field.from];
95
+ }
96
+ if (field.transform) {
97
+ value = applyTransforms(value, field.transform, outputName, row);
98
+ }
99
+ output[outputName] = value;
100
+ applyNullHandling(output, outputName, field);
101
+ }
102
+ return output;
103
+ }
104
+ function replaceNullSentinels(row, sentinels) {
105
+ for (const key of Object.keys(row)) {
106
+ if (sentinels.includes(row[key])) {
107
+ row[key] = null;
108
+ }
109
+ }
110
+ }
111
+ function applyNullHandling(output, key, field) {
112
+ const value = output[key];
113
+ if (value === void 0 && field.undefinedAs !== void 0) {
114
+ output[key] = field.undefinedAs;
115
+ return;
116
+ }
117
+ if (value === null && field.nullAs !== void 0) {
118
+ if (field.nullAs === "omit") {
119
+ delete output[key];
120
+ } else if (field.nullAs === "undefined") {
121
+ output[key] = void 0;
122
+ } else {
123
+ output[key] = field.nullAs;
124
+ }
125
+ }
126
+ }
127
+ export {
128
+ applyFieldMapping,
129
+ applyTransforms,
130
+ executeTransform
131
+ };
132
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/transforms.ts","../src/apply.ts"],"sourcesContent":["import type { BuiltinTransform, TransformSpec } from '@semilayer/core'\n\n/**\n * Execute a single transform on a value.\n * Never throws — data massaging, not validation.\n */\nexport function executeTransform(\n value: unknown,\n transform: BuiltinTransform,\n field?: string,\n row?: Record<string, unknown>,\n): unknown {\n try {\n switch (transform.type) {\n case 'toString':\n return value == null ? '' : String(value)\n\n case 'toNumber': {\n if (value == null) return NaN\n const n = Number(value)\n return n\n }\n\n case 'toBoolean': {\n if (value == null) return false\n if (typeof value === 'boolean') return value\n if (typeof value === 'number') return value !== 0\n const s = String(value).toLowerCase().trim()\n return s === 'true' || s === '1' || s === 'yes'\n }\n\n case 'toDate': {\n if (value == null) return null\n const d = new Date(value as string | number)\n return isNaN(d.getTime()) ? null : d.toISOString()\n }\n\n case 'round': {\n const num = typeof value === 'number' ? value : Number(value)\n if (isNaN(num)) return value\n const decimals = transform.decimals ?? 0\n const factor = Math.pow(10, decimals)\n const mode = transform.mode ?? 'round'\n return Math[mode](num * factor) / factor\n }\n\n case 'trim':\n return value == null ? '' : String(value).trim()\n\n case 'lowercase':\n return value == null ? '' : String(value).toLowerCase()\n\n case 'uppercase':\n return value == null ? '' : String(value).toUpperCase()\n\n case 'default':\n return value == null ? transform.value : value\n\n case 'split': {\n if (value == null) return []\n return String(value).split(transform.separator)\n }\n\n case 'join': {\n if (Array.isArray(value)) return value.join(transform.separator)\n return value == null ? '' : String(value)\n }\n\n case 'truncate': {\n if (value == null) return ''\n return String(value).slice(0, transform.length)\n }\n\n case 'replace': {\n if (value == null) return ''\n return String(value).replace(new RegExp(transform.pattern, 'g'), transform.replacement)\n }\n\n case 'custom': {\n // eslint-disable-next-line no-new-func\n const fn = new Function('value', 'field', 'row', transform.body)\n return fn(value, field, row)\n }\n\n default:\n return value\n }\n } catch {\n // Data massaging — never throw\n return value\n }\n}\n\n/**\n * Execute a transform spec (single or chain) on a value.\n */\nexport function applyTransforms(\n value: unknown,\n spec: TransformSpec,\n field?: string,\n row?: Record<string, unknown>,\n): unknown {\n const transforms = Array.isArray(spec) ? spec : [spec]\n let result = value\n for (const t of transforms) {\n result = executeTransform(result, t, field, row)\n }\n return result\n}\n","import type { FieldConfig } from '@semilayer/core'\nimport { applyTransforms } from './transforms.js'\n\nexport interface ApplyFieldMappingOptions {\n nullValues?: unknown[]\n}\n\n/**\n * Applies field definitions to a raw source row, producing the output row.\n * Each field resolves its source value (from `from` or identity), applies\n * merge/transform/null handling, and writes to the output under the field key.\n */\nexport function applyFieldMapping(\n row: Record<string, unknown>,\n fields: Record<string, FieldConfig>,\n opts?: ApplyFieldMappingOptions,\n): Record<string, unknown> {\n // Phase 1: null sentinel replacement on raw row\n if (opts?.nullValues && opts.nullValues.length > 0) {\n replaceNullSentinels(row, opts.nullValues)\n }\n\n const output: Record<string, unknown> = {}\n\n // Phase 2: process each field definition\n for (const [outputName, field] of Object.entries(fields)) {\n let value: unknown\n\n if (field.from === undefined) {\n // Identity: source column = output name\n value = row[outputName]\n } else if (Array.isArray(field.from)) {\n // Multi-source merge\n if (field.merge === 'coalesce') {\n value = field.from.reduce<unknown>((acc, src) => acc ?? row[src], undefined)\n } else {\n // concat (default for multi-source)\n const parts = field.from.map((src) => row[src]).filter((v) => v != null)\n value = parts.map(String).join(field.separator ?? ' ')\n }\n } else {\n // Single rename: from is a string\n value = row[field.from]\n }\n\n // Apply transform chain\n if (field.transform) {\n value = applyTransforms(value, field.transform, outputName, row)\n }\n\n output[outputName] = value\n\n // Null/undefined handling\n applyNullHandling(output, outputName, field)\n }\n\n return output\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction replaceNullSentinels(row: Record<string, unknown>, sentinels: unknown[]): void {\n for (const key of Object.keys(row)) {\n if (sentinels.includes(row[key])) {\n row[key] = null\n }\n }\n}\n\nfunction applyNullHandling(\n output: Record<string, unknown>,\n key: string,\n field: FieldConfig,\n): void {\n const value = output[key]\n\n // undefinedAs: handle missing/undefined values\n if (value === undefined && field.undefinedAs !== undefined) {\n output[key] = field.undefinedAs\n return\n }\n\n // nullAs: handle null values\n if (value === null && field.nullAs !== undefined) {\n if (field.nullAs === 'omit') {\n delete output[key]\n } else if (field.nullAs === 'undefined') {\n output[key] = undefined\n } else {\n output[key] = field.nullAs\n }\n }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@semilayer/data-mapping",
3
+ "version": "1.1.0",
4
+ "description": "SemiLayer data mapping — field renaming, transforms, null handling",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsup",
19
+ "dev": "tsup --watch",
20
+ "lint": "eslint src/",
21
+ "typecheck": "tsc --noEmit",
22
+ "test": "vitest run"
23
+ },
24
+ "dependencies": {
25
+ "@semilayer/core": "workspace:*"
26
+ },
27
+ "devDependencies": {
28
+ "tsup": "^8.0.0",
29
+ "typescript": "^5.7.0",
30
+ "vitest": "^3.0.0"
31
+ }
32
+ }