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.
- package/.env.example +16 -0
- package/LICENSE +21 -0
- package/README.md +208 -0
- package/dist/browser-automation.d.ts +24 -0
- package/dist/browser-automation.d.ts.map +1 -0
- package/dist/browser-automation.js +295 -0
- package/dist/browser-automation.js.map +1 -0
- package/dist/cli-browser.d.ts +3 -0
- package/dist/cli-browser.d.ts.map +1 -0
- package/dist/cli-browser.js +134 -0
- package/dist/cli-browser.js.map +1 -0
- package/dist/cli-fixed.d.ts +3 -0
- package/dist/cli-fixed.d.ts.map +1 -0
- package/dist/cli-fixed.js +112 -0
- package/dist/cli-fixed.js.map +1 -0
- package/dist/cli-simple.d.ts +3 -0
- package/dist/cli-simple.d.ts.map +1 -0
- package/dist/cli-simple.js +167 -0
- package/dist/cli-simple.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +167 -0
- package/dist/cli.js.map +1 -0
- package/dist/load-graphql.d.ts +23 -0
- package/dist/load-graphql.d.ts.map +1 -0
- package/dist/load-graphql.js +239 -0
- package/dist/load-graphql.js.map +1 -0
- package/dist/load-old.d.ts +24 -0
- package/dist/load-old.d.ts.map +1 -0
- package/dist/load-old.js +183 -0
- package/dist/load-old.js.map +1 -0
- package/dist/load-real.d.ts +23 -0
- package/dist/load-real.d.ts.map +1 -0
- package/dist/load-real.js +202 -0
- package/dist/load-real.js.map +1 -0
- package/dist/load.d.ts +24 -0
- package/dist/load.d.ts.map +1 -0
- package/dist/load.js +195 -0
- package/dist/load.js.map +1 -0
- package/dist/mapper.d.ts +16 -0
- package/dist/mapper.d.ts.map +1 -0
- package/dist/mapper.js +181 -0
- package/dist/mapper.js.map +1 -0
- package/dist/parser-broken.d.ts +11 -0
- package/dist/parser-broken.d.ts.map +1 -0
- package/dist/parser-broken.js +88 -0
- package/dist/parser-broken.js.map +1 -0
- package/dist/parser-old.d.ts +11 -0
- package/dist/parser-old.d.ts.map +1 -0
- package/dist/parser-old.js +90 -0
- package/dist/parser-old.js.map +1 -0
- package/dist/parser.d.ts +11 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +83 -0
- package/dist/parser.js.map +1 -0
- package/dist/reporter.d.ts +15 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +144 -0
- package/dist/reporter.js.map +1 -0
- package/examples/contacts-mapping.txt +8 -0
- package/examples/contacts.csv +6 -0
- package/package.json +50 -0
- package/src/browser-automation.ts +350 -0
- package/src/cli-browser.ts +134 -0
- package/src/cli-simple.ts +158 -0
- package/src/cli.ts +159 -0
- package/src/load-graphql.ts +238 -0
- package/src/load-old.ts +177 -0
- package/src/load-real.ts +199 -0
- package/src/load.ts +197 -0
- package/src/mapper.ts +183 -0
- package/src/parser.ts +55 -0
- package/src/reporter.ts +131 -0
- package/tsconfig.json +24 -0
package/src/load-real.ts
ADDED
|
@@ -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
|
+
}
|