twenty-import-csv 1.0.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.
Files changed (74) hide show
  1. package/.env.example +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +208 -0
  4. package/dist/browser-automation.d.ts +24 -0
  5. package/dist/browser-automation.d.ts.map +1 -0
  6. package/dist/browser-automation.js +295 -0
  7. package/dist/browser-automation.js.map +1 -0
  8. package/dist/cli-browser.d.ts +3 -0
  9. package/dist/cli-browser.d.ts.map +1 -0
  10. package/dist/cli-browser.js +134 -0
  11. package/dist/cli-browser.js.map +1 -0
  12. package/dist/cli-fixed.d.ts +3 -0
  13. package/dist/cli-fixed.d.ts.map +1 -0
  14. package/dist/cli-fixed.js +112 -0
  15. package/dist/cli-fixed.js.map +1 -0
  16. package/dist/cli-simple.d.ts +3 -0
  17. package/dist/cli-simple.d.ts.map +1 -0
  18. package/dist/cli-simple.js +167 -0
  19. package/dist/cli-simple.js.map +1 -0
  20. package/dist/cli.d.ts +3 -0
  21. package/dist/cli.d.ts.map +1 -0
  22. package/dist/cli.js +167 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/load-graphql.d.ts +23 -0
  25. package/dist/load-graphql.d.ts.map +1 -0
  26. package/dist/load-graphql.js +239 -0
  27. package/dist/load-graphql.js.map +1 -0
  28. package/dist/load-old.d.ts +24 -0
  29. package/dist/load-old.d.ts.map +1 -0
  30. package/dist/load-old.js +183 -0
  31. package/dist/load-old.js.map +1 -0
  32. package/dist/load-real.d.ts +23 -0
  33. package/dist/load-real.d.ts.map +1 -0
  34. package/dist/load-real.js +202 -0
  35. package/dist/load-real.js.map +1 -0
  36. package/dist/load.d.ts +24 -0
  37. package/dist/load.d.ts.map +1 -0
  38. package/dist/load.js +195 -0
  39. package/dist/load.js.map +1 -0
  40. package/dist/mapper.d.ts +16 -0
  41. package/dist/mapper.d.ts.map +1 -0
  42. package/dist/mapper.js +181 -0
  43. package/dist/mapper.js.map +1 -0
  44. package/dist/parser-broken.d.ts +11 -0
  45. package/dist/parser-broken.d.ts.map +1 -0
  46. package/dist/parser-broken.js +88 -0
  47. package/dist/parser-broken.js.map +1 -0
  48. package/dist/parser-old.d.ts +11 -0
  49. package/dist/parser-old.d.ts.map +1 -0
  50. package/dist/parser-old.js +90 -0
  51. package/dist/parser-old.js.map +1 -0
  52. package/dist/parser.d.ts +11 -0
  53. package/dist/parser.d.ts.map +1 -0
  54. package/dist/parser.js +83 -0
  55. package/dist/parser.js.map +1 -0
  56. package/dist/reporter.d.ts +15 -0
  57. package/dist/reporter.d.ts.map +1 -0
  58. package/dist/reporter.js +144 -0
  59. package/dist/reporter.js.map +1 -0
  60. package/examples/contacts-mapping.txt +8 -0
  61. package/examples/contacts.csv +6 -0
  62. package/package.json +50 -0
  63. package/src/browser-automation.ts +350 -0
  64. package/src/cli-browser.ts +134 -0
  65. package/src/cli-simple.ts +158 -0
  66. package/src/cli.ts +159 -0
  67. package/src/load-graphql.ts +238 -0
  68. package/src/load-old.ts +177 -0
  69. package/src/load-real.ts +199 -0
  70. package/src/load.ts +197 -0
  71. package/src/mapper.ts +183 -0
  72. package/src/parser.ts +55 -0
  73. package/src/reporter.ts +131 -0
  74. package/tsconfig.json +24 -0
@@ -0,0 +1,199 @@
1
+ import axios, { AxiosInstance } from 'axios';
2
+ import { ParsedRecord } from './parser';
3
+ import * as fs from 'fs';
4
+
5
+ export interface LoadResult {
6
+ success: number;
7
+ errors: number;
8
+ errorLog: string[];
9
+ }
10
+
11
+ export interface BatchResult {
12
+ success: boolean;
13
+ count: number;
14
+ error?: string;
15
+ }
16
+
17
+ export class TwentyRealAPIClient {
18
+ private client: AxiosInstance;
19
+ private baseUrl: string;
20
+ private apiKey: string;
21
+
22
+ constructor(baseUrl: string, apiKey: string) {
23
+ this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
24
+ this.apiKey = apiKey;
25
+
26
+ this.client = axios.create({
27
+ baseURL: `${this.baseUrl}`,
28
+ headers: {
29
+ 'Authorization': `Bearer ${this.apiKey}`,
30
+ 'Content-Type': 'application/json'
31
+ },
32
+ timeout: 30000
33
+ });
34
+ }
35
+
36
+ async testConnection(): Promise<boolean> {
37
+ try {
38
+ // ŠŸŃ€Š¾Š²ŠµŃ€ŃŠµŠ¼ health endpoint
39
+ const response = await this.client.get('/api/ping');
40
+ return response.status === 200;
41
+ } catch (error: any) {
42
+ console.error('āŒ Connection test failed:', error.response?.data || error.message);
43
+ return false;
44
+ }
45
+ }
46
+
47
+ async createRecords(objectType: string, records: ParsedRecord[]): Promise<BatchResult> {
48
+ try {
49
+ // ŠŸŃ€Š¾Š±ŃƒŠµŠ¼ разные Ń€ŠµŠ°Š»ŃŒŠ½Ń‹Šµ endpoints Twenty CRM
50
+ const endpoints = [
51
+ `/api/${objectType}`,
52
+ `/api/v1/${objectType}`,
53
+ `/api/rest/${objectType}`,
54
+ `/v1/${objectType}`,
55
+ `/${objectType}`
56
+ ];
57
+
58
+ for (const endpoint of endpoints) {
59
+ try {
60
+ console.log(`šŸ” ŠŸŃ€Š¾Š±ŃƒŠµŠ¼ endpoint: ${endpoint}`);
61
+
62
+ // Формат payload Š“Š»Ń Twenty CRM
63
+ const payload = records.map(record => {
64
+ const transformed: any = {};
65
+
66
+ if (objectType === 'people') {
67
+ // ŠŸŃ€Š¾Š±ŃƒŠµŠ¼ разные форматы name
68
+ if (record['name.firstName'] || record['name.lastName']) {
69
+ transformed.name = {
70
+ firstName: record['name.firstName'] || '',
71
+ lastName: record['name.lastName'] || ''
72
+ };
73
+ }
74
+
75
+ if (record.email) transformed.email = record.email;
76
+ if (record.phone) transformed.phone = record.phone;
77
+ if (record.jobTitle) transformed.jobTitle = record.jobTitle;
78
+ if (record.city) transformed.city = record.city;
79
+ if (record.country) transformed.country = record.country;
80
+ }
81
+
82
+ return transformed;
83
+ });
84
+
85
+ const response = await this.client.post(endpoint, payload);
86
+
87
+ console.log(`āœ… Endpoint ${endpoint} работает!`);
88
+ return {
89
+ success: true,
90
+ count: records.length
91
+ };
92
+
93
+ } catch (error: any) {
94
+ console.log(`āŒ Endpoint ${endpoint} не работает:`, error.response?.status);
95
+ if (error.response?.status !== 404) {
96
+ // Если ŃŃ‚Š¾ не 404, ŠæŃ€Š¾Š±ŃƒŠµŠ¼ ŃŠ»ŠµŠ“ŃƒŃŽŃ‰ŠøŠ¹ endpoint
97
+ continue;
98
+ }
99
+ }
100
+ }
101
+
102
+ throw new Error(`ŠŠµ найГен рабочий endpoint Š“Š»Ń ${objectType}`);
103
+
104
+ } catch (error: any) {
105
+ return {
106
+ success: false,
107
+ count: 0,
108
+ error: error.response?.data?.message || error.message
109
+ };
110
+ }
111
+ }
112
+
113
+ getObjectEndpoint(objectType: string): string {
114
+ const endpoints: { [key: string]: string } = {
115
+ 'people': 'people',
116
+ 'persons': 'people',
117
+ 'contacts': 'people',
118
+ 'companies': 'companies',
119
+ 'organization': 'companies',
120
+ 'opportunities': 'opportunities',
121
+ 'deals': 'opportunities',
122
+ 'tasks': 'tasks',
123
+ 'notes': 'notes'
124
+ };
125
+
126
+ return endpoints[objectType.toLowerCase()] || objectType;
127
+ }
128
+ }
129
+
130
+ export async function loadToTwenty(
131
+ data: ParsedRecord[],
132
+ objectType: string,
133
+ twentyUrl: string,
134
+ twentyKey: string,
135
+ batchSize: number,
136
+ progressBar: any
137
+ ): Promise<LoadResult> {
138
+ const client = new TwentyRealAPIClient(twentyUrl, twentyKey);
139
+ const errorLog: string[] = [];
140
+ let successCount = 0;
141
+ let errorCount = 0;
142
+
143
+ // Test connection first
144
+ console.log('šŸ”— Testing Twenty CRM connection...');
145
+ const isConnected = await client.testConnection();
146
+ if (!isConnected) {
147
+ throw new Error('Failed to connect to Twenty CRM. Check URL and API key.');
148
+ }
149
+ console.log('āœ… Connection successful');
150
+
151
+ console.log(`šŸ“¤ Loading ${data.length} records to Twenty CRM...`);
152
+
153
+ // Process in batches
154
+ for (let i = 0; i < data.length; i += batchSize) {
155
+ const batch = data.slice(i, i + batchSize);
156
+ const batchNumber = Math.floor(i / batchSize) + 1;
157
+ const totalBatches = Math.ceil(data.length / batchSize);
158
+
159
+ try {
160
+ progressBar.update((i / data.length) * 100, {
161
+ batch: `${batchNumber}/${totalBatches}`,
162
+ records: `${i + 1}-${Math.min(i + batchSize, data.length)}`
163
+ });
164
+
165
+ const batchResult = await client.createRecords(objectType, batch);
166
+
167
+ if (batchResult.success) {
168
+ successCount += batchResult.count;
169
+ console.log(`āœ… Batch ${batchNumber} imported successfully`);
170
+ } else {
171
+ errorCount += batch.length;
172
+ const errorMsg = `Batch ${batchNumber}: ${batchResult.error}`;
173
+ errorLog.push(errorMsg);
174
+ console.error(`āŒ ${errorMsg}`);
175
+ }
176
+ } catch (error: any) {
177
+ const errorMsg = `Batch ${batchNumber}: ${error.message}`;
178
+ errorLog.push(errorMsg);
179
+ errorCount += batch.length;
180
+ }
181
+
182
+ // Small delay to avoid rate limiting
183
+ await new Promise(resolve => setTimeout(resolve, 100));
184
+ }
185
+
186
+ return {
187
+ success: successCount,
188
+ errors: errorCount,
189
+ errorLog
190
+ };
191
+ }
192
+
193
+ export function saveErrorLog(errorLog: string[], filename: string): void {
194
+ if (errorLog.length === 0) return;
195
+
196
+ const logContent = errorLog.join('\n');
197
+ fs.writeFileSync(filename, logContent, 'utf8');
198
+ console.log(`šŸ“„ Error log saved to ${filename}`);
199
+ }
package/src/load.ts ADDED
@@ -0,0 +1,197 @@
1
+ import axios, { AxiosInstance } from 'axios';
2
+ import { ParsedRecord } from './parser';
3
+ import * as fs from 'fs';
4
+
5
+ export interface LoadResult {
6
+ success: number;
7
+ errors: number;
8
+ errorLog: string[];
9
+ }
10
+
11
+ export interface BatchResult {
12
+ success: boolean;
13
+ count: number;
14
+ error?: string;
15
+ }
16
+
17
+ export class TwentyAPIClient {
18
+ private client: AxiosInstance;
19
+ private baseUrl: string;
20
+ private apiKey: string;
21
+
22
+ constructor(baseUrl: string, apiKey: string) {
23
+ this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
24
+ this.apiKey = apiKey;
25
+
26
+ this.client = axios.create({
27
+ baseURL: `${this.baseUrl}/rest`,
28
+ headers: {
29
+ 'Authorization': `Bearer ${this.apiKey}`,
30
+ 'Content-Type': 'application/json'
31
+ },
32
+ timeout: 30000
33
+ });
34
+ }
35
+
36
+ async testConnection(): Promise<boolean> {
37
+ try {
38
+ const response = await this.client.get('/people', { params: { limit: 1 } });
39
+ return response.status === 200;
40
+ } catch (error: any) {
41
+ console.error('āŒ Connection test failed:', error.response?.data || error.message);
42
+ return false;
43
+ }
44
+ }
45
+
46
+ async createRecords(objectType: string, records: ParsedRecord[]): Promise<BatchResult> {
47
+ try {
48
+ // Try different payload formats
49
+ let payload;
50
+
51
+ // Format 1: { data: records }
52
+ payload = { data: records };
53
+
54
+ // Format 2: records directly
55
+ // payload = records;
56
+
57
+ // Format 3: { records: records }
58
+ // payload = { records: records };
59
+
60
+ console.log('šŸ” Debug - Payload format:', JSON.stringify(payload, null, 2));
61
+ console.log('šŸ” Debug - Payload length:', JSON.stringify(payload).length);
62
+
63
+ const response = await this.client.post(`/${objectType}`, payload);
64
+
65
+ return {
66
+ success: true,
67
+ count: records.length
68
+ };
69
+ } catch (error: any) {
70
+ console.error('šŸ” Debug - Error response:', error.response?.data);
71
+ console.error('šŸ” Debug - Error status:', error.response?.status);
72
+
73
+ return {
74
+ success: false,
75
+ count: 0,
76
+ error: error.response?.data?.message || error.message
77
+ };
78
+ }
79
+ }
80
+
81
+ async createRecord(objectType: string, record: ParsedRecord): Promise<BatchResult> {
82
+ try {
83
+ console.log('šŸ” Debug - Single record:', JSON.stringify(record, null, 2));
84
+
85
+ const response = await this.client.post(`/${objectType}`, record);
86
+ return {
87
+ success: true,
88
+ count: 1
89
+ };
90
+ } catch (error: any) {
91
+ console.error('šŸ” Debug - Single record error:', error.response?.data);
92
+
93
+ return {
94
+ success: false,
95
+ count: 0,
96
+ error: error.response?.data?.message || error.message
97
+ };
98
+ }
99
+ }
100
+
101
+ getObjectEndpoint(objectType: string): string {
102
+ const endpoints: { [key: string]: string } = {
103
+ 'people': 'people',
104
+ 'persons': 'people',
105
+ 'contacts': 'people',
106
+ 'companies': 'companies',
107
+ 'organization': 'companies',
108
+ 'opportunities': 'opportunities',
109
+ 'deals': 'opportunities',
110
+ 'tasks': 'tasks',
111
+ 'notes': 'notes'
112
+ };
113
+
114
+ return endpoints[objectType.toLowerCase()] || objectType;
115
+ }
116
+ }
117
+
118
+ export async function loadToTwenty(
119
+ data: ParsedRecord[],
120
+ objectType: string,
121
+ twentyUrl: string,
122
+ twentyKey: string,
123
+ batchSize: number,
124
+ progressBar: any
125
+ ): Promise<LoadResult> {
126
+ const client = new TwentyAPIClient(twentyUrl, twentyKey);
127
+ const errorLog: string[] = [];
128
+ let successCount = 0;
129
+ let errorCount = 0;
130
+
131
+ // Test connection first
132
+ console.log('šŸ”— Testing Twenty CRM connection...');
133
+ const isConnected = await client.testConnection();
134
+ if (!isConnected) {
135
+ throw new Error('Failed to connect to Twenty CRM. Check URL and API key.');
136
+ }
137
+ console.log('āœ… Connection successful');
138
+
139
+ const endpoint = client.getObjectEndpoint(objectType);
140
+ console.log(`šŸ“¤ Loading ${data.length} records to ${endpoint}...`);
141
+
142
+ // Process in batches
143
+ for (let i = 0; i < data.length; i += batchSize) {
144
+ const batch = data.slice(i, i + batchSize);
145
+ const batchNumber = Math.floor(i / batchSize) + 1;
146
+ const totalBatches = Math.ceil(data.length / batchSize);
147
+
148
+ try {
149
+ progressBar.update((i / data.length) * 100, {
150
+ batch: `${batchNumber}/${totalBatches}`,
151
+ records: `${i + 1}-${Math.min(i + batchSize, data.length)}`
152
+ });
153
+
154
+ // Try batch create first
155
+ const batchResult = await client.createRecords(endpoint, batch);
156
+
157
+ if (batchResult.success) {
158
+ successCount += batchResult.count;
159
+ } else {
160
+ // Fallback to individual records
161
+ console.warn(`āš ļø Batch create failed, trying individual records...`);
162
+
163
+ for (const record of batch) {
164
+ const result = await client.createRecord(endpoint, record);
165
+ if (result.success) {
166
+ successCount++;
167
+ } else {
168
+ errorCount++;
169
+ const errorMsg = `Record ${JSON.stringify(record)}: ${result.error}`;
170
+ errorLog.push(errorMsg);
171
+ }
172
+ }
173
+ }
174
+ } catch (error: any) {
175
+ const errorMsg = `Batch ${batchNumber}: ${error.message}`;
176
+ errorLog.push(errorMsg);
177
+ errorCount += batch.length;
178
+ }
179
+
180
+ // Small delay to avoid rate limiting
181
+ await new Promise(resolve => setTimeout(resolve, 100));
182
+ }
183
+
184
+ return {
185
+ success: successCount,
186
+ errors: errorCount,
187
+ errorLog
188
+ };
189
+ }
190
+
191
+ export function saveErrorLog(errorLog: string[], filename: string): void {
192
+ if (errorLog.length === 0) return;
193
+
194
+ const logContent = errorLog.join('\n');
195
+ fs.writeFileSync(filename, logContent, 'utf8');
196
+ console.log(`šŸ“„ Error log saved to ${filename}`);
197
+ }
package/src/mapper.ts ADDED
@@ -0,0 +1,183 @@
1
+ import * as fs from 'fs';
2
+ import * as readline from 'readline';
3
+ import { ParsedRecord } from './parser';
4
+
5
+ export interface FieldMapping {
6
+ [csvField: string]: string;
7
+ }
8
+
9
+ export interface TwentyObjectFields {
10
+ people: string[];
11
+ companies: string[];
12
+ opportunities: string[];
13
+ tasks: string[];
14
+ notes: string[];
15
+ }
16
+
17
+ // Twenty CRM API field mappings
18
+ const TWENTY_FIELDS: TwentyObjectFields = {
19
+ people: [
20
+ 'name.firstName', 'name.lastName', 'email', 'phone', 'jobTitle',
21
+ 'city', 'country', 'linkedinUrl', 'avatarUrl', 'note'
22
+ ],
23
+ companies: [
24
+ 'name', 'domainName', 'employees', 'annualRevenue', 'industry',
25
+ 'linkedinUrl', 'phone', 'address', 'city', 'country'
26
+ ],
27
+ opportunities: [
28
+ 'name', 'amount', 'currency', 'stage', 'probability',
29
+ 'closeDate', 'description', 'companyId', 'personId'
30
+ ],
31
+ tasks: [
32
+ 'title', 'description', 'status', 'dueDate', 'assigneeId',
33
+ 'companyId', 'personId', 'priority'
34
+ ],
35
+ notes: [
36
+ 'body', 'authorId', 'companyId', 'personId', 'opportunityId'
37
+ ]
38
+ };
39
+
40
+ export async function loadMapping(mappingFile: string): Promise<FieldMapping> {
41
+ return new Promise((resolve, reject) => {
42
+ const mapping: FieldMapping = {};
43
+
44
+ if (!fs.existsSync(mappingFile)) {
45
+ reject(new Error(`Mapping file ${mappingFile} not found`));
46
+ return;
47
+ }
48
+
49
+ const rl = readline.createInterface({
50
+ input: fs.createReadStream(mappingFile),
51
+ crlfDelay: Infinity
52
+ });
53
+
54
+ rl.on('line', (line) => {
55
+ const [csvField, twentyField] = line.split('=').map(s => s.trim());
56
+ if (csvField && twentyField) {
57
+ mapping[csvField] = twentyField;
58
+ }
59
+ });
60
+
61
+ rl.on('close', () => {
62
+ resolve(mapping);
63
+ });
64
+
65
+ rl.on('error', (error) => {
66
+ reject(error);
67
+ });
68
+ });
69
+ }
70
+
71
+ export async function mapFields(
72
+ data: ParsedRecord[],
73
+ objectType: string,
74
+ mapping?: FieldMapping
75
+ ): Promise<ParsedRecord[]> {
76
+ const validFields = TWENTY_FIELDS[objectType as keyof TwentyObjectFields];
77
+
78
+ if (!validFields) {
79
+ throw new Error(`Unknown object type: ${objectType}`);
80
+ }
81
+
82
+ return data.map(record => {
83
+ const mapped: ParsedRecord = {};
84
+
85
+ Object.keys(record).forEach(csvField => {
86
+ const twentyField = mapping ? mapping[csvField] : csvField;
87
+
88
+ // Auto-map common field names
89
+ const autoMappedField = autoMapField(csvField, objectType);
90
+ const finalField = twentyField || autoMappedField;
91
+
92
+ if (finalField && validFields.includes(finalField)) {
93
+ mapped[finalField] = record[csvField];
94
+ }
95
+ });
96
+
97
+ return mapped;
98
+ });
99
+ }
100
+
101
+ function autoMapField(csvField: string, objectType: string): string | null {
102
+ const field = csvField.toLowerCase();
103
+
104
+ const commonMappings: { [key: string]: { [key: string]: string } } = {
105
+ people: {
106
+ 'first_name': 'name.firstName',
107
+ 'last_name': 'name.lastName',
108
+ 'firstname': 'name.firstName',
109
+ 'lastname': 'name.lastName',
110
+ 'full_name': 'name.firstName',
111
+ 'email_address': 'email',
112
+ 'phone_number': 'phone',
113
+ 'job_title': 'jobTitle',
114
+ 'company': 'company',
115
+ 'linkedin': 'linkedinUrl',
116
+ 'avatar': 'avatarUrl'
117
+ },
118
+ companies: {
119
+ 'company_name': 'name',
120
+ 'company': 'name',
121
+ 'website': 'domainName',
122
+ 'domain': 'domainName',
123
+ 'employee_count': 'employees',
124
+ 'revenue': 'annualRevenue',
125
+ 'address_line': 'address',
126
+ 'zip_code': 'zipCode'
127
+ },
128
+ opportunities: {
129
+ 'deal_name': 'name',
130
+ 'deal': 'name',
131
+ 'amount': 'amount',
132
+ 'value': 'amount',
133
+ 'stage': 'stage',
134
+ 'probability': 'probability',
135
+ 'close_date': 'closeDate',
136
+ 'description': 'description'
137
+ }
138
+ };
139
+
140
+ return commonMappings[objectType]?.[field] || null;
141
+ }
142
+
143
+ export async function interactiveMapping(
144
+ data: ParsedRecord[],
145
+ objectType: string
146
+ ): Promise<FieldMapping> {
147
+ const mapping: FieldMapping = {};
148
+ const validFields = TWENTY_FIELDS[objectType as keyof TwentyObjectFields];
149
+ const detectedFields = Object.keys(data[0] || {});
150
+
151
+ console.log(`\nšŸ—ŗ Interactive Field Mapping for ${objectType}`);
152
+ console.log(`šŸ“‹ Available Twenty fields: ${validFields.join(', ')}`);
153
+ console.log(`šŸ“‹ Detected CSV fields: ${detectedFields.join(', ')}\n`);
154
+
155
+ const rl = readline.createInterface({
156
+ input: process.stdin,
157
+ output: process.stdout
158
+ });
159
+
160
+ for (const csvField of detectedFields) {
161
+ const answer = await new Promise<string>((resolve) => {
162
+ rl.question(`Map "${csvField}" -> (field name or skip): `, resolve);
163
+ });
164
+
165
+ if (answer && answer !== 'skip' && validFields.includes(answer)) {
166
+ mapping[csvField] = answer;
167
+ console.log(`āœ… ${csvField} → ${answer}`);
168
+ } else if (answer === 'skip') {
169
+ console.log(`ā­ļø Skipping ${csvField}`);
170
+ } else {
171
+ console.log(`āŒ Invalid field. Skipping ${csvField}`);
172
+ }
173
+ }
174
+
175
+ rl.close();
176
+ return mapping;
177
+ }
178
+
179
+ export function saveMapping(mapping: FieldMapping, outputFile: string): void {
180
+ const lines = Object.entries(mapping).map(([csv, twenty]) => `${csv}=${twenty}`);
181
+ fs.writeFileSync(outputFile, lines.join('\n'), 'utf8');
182
+ console.log(`šŸ’¾ Mapping saved to ${outputFile}`);
183
+ }
package/src/parser.ts ADDED
@@ -0,0 +1,55 @@
1
+ import { readFileSync, statSync } from 'fs';
2
+ import { basename } from 'path';
3
+ import * as Papa from 'papaparse';
4
+
5
+ export interface ParsedRecord {
6
+ [key: string]: any;
7
+ }
8
+
9
+ export async function parseCSV(filePath: string): Promise<ParsedRecord[]> {
10
+ return new Promise((resolve, reject) => {
11
+ Papa.parse(readFileSync(filePath, 'utf8'), {
12
+ header: true,
13
+ skipEmptyLines: true,
14
+ complete: (results) => {
15
+ if (results.errors.length > 0) {
16
+ console.warn(`āš ļø CSV parsing warnings: ${results.errors.length}`);
17
+ results.errors.forEach((error: any) => {
18
+ console.warn(` Row ${error.row}: ${error.message}`);
19
+ });
20
+ }
21
+
22
+ resolve(results.data as ParsedRecord[]);
23
+ },
24
+ error: (error: any) => {
25
+ reject(new Error(`CSV parsing failed: ${error.message}`));
26
+ }
27
+ });
28
+ });
29
+ }
30
+
31
+ export function detectColumns(data: ParsedRecord[]): string[] {
32
+ if (data.length === 0) return [];
33
+
34
+ const allFields = new Set<string>();
35
+
36
+ data.forEach(record => {
37
+ Object.keys(record).forEach(key => {
38
+ if (record[key] !== null && record[key] !== undefined && record[key] !== '') {
39
+ allFields.add(key);
40
+ }
41
+ });
42
+ });
43
+
44
+ return Array.from(allFields).sort();
45
+ }
46
+
47
+ export function getFileInfo(filePath: string): { name: string; size: number; rows: number } {
48
+ const stats = statSync(filePath);
49
+
50
+ return {
51
+ name: basename(filePath),
52
+ size: stats.size,
53
+ rows: 0 // Will be updated after parsing
54
+ };
55
+ }