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
package/src/cli.ts ADDED
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { loadToTwenty } from './load-real';
4
+ import { Command } from 'commander';
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import * as Papa from 'papaparse';
8
+ import axios from 'axios';
9
+
10
+ interface Options {
11
+ file: string;
12
+ object: string;
13
+ twentyUrl: string;
14
+ twentyKey: string;
15
+ dryRun: boolean;
16
+ batch?: number;
17
+ }
18
+
19
+ const program = new Command();
20
+
21
+ program
22
+ .name('twenty-import-csv')
23
+ .description('Universal CSV import tool for Twenty CRM')
24
+ .version('1.0.0');
25
+
26
+ program
27
+ .requiredOption('-f, --file <path>', 'CSV file to import')
28
+ .requiredOption('-o, --object <type>', 'Twenty object type (people, companies, opportunities, etc.)')
29
+ .requiredOption('-u, --twenty-url <url>', 'Twenty CRM URL')
30
+ .requiredOption('-k, --twenty-key <key>', 'Twenty CRM API key')
31
+ .option('-d, --dry-run', 'Preview import without writing data', false)
32
+ .option('-b, --batch <number>', 'Batch size for API calls', '60')
33
+ .parse();
34
+
35
+ const options = program.opts() as Options;
36
+
37
+ async function parseCSV(filePath: string): Promise<any[]> {
38
+ return new Promise((resolve, reject) => {
39
+ const fileContent = fs.readFileSync(filePath, 'utf8');
40
+
41
+ Papa.parse(fileContent, {
42
+ header: true,
43
+ skipEmptyLines: true,
44
+ complete: (results) => {
45
+ if (results.errors.length > 0) {
46
+ console.warn(`⚠️ CSV parsing warnings: ${results.errors.length}`);
47
+ }
48
+ resolve(results.data);
49
+ },
50
+ error: (error: any) => {
51
+ reject(new Error(`CSV parsing failed: ${error.message}`));
52
+ }
53
+ });
54
+ });
55
+ }
56
+
57
+ async function main() {
58
+ try {
59
+ console.log('🚀 Twenty CSV Import Tool');
60
+ console.log(`📁 File: ${options.file}`);
61
+ console.log(`🎯 Object: ${options.object}`);
62
+ console.log(`🔗 Twenty URL: ${options.twentyUrl}`);
63
+ console.log(`🧪 Dry Run: ${options.dryRun ? 'YES' : 'NO'}`);
64
+
65
+ // Check if file exists
66
+ if (!fs.existsSync(options.file)) {
67
+ console.error(`❌ Error: File ${options.file} not found`);
68
+ process.exit(1);
69
+ }
70
+
71
+ // Parse CSV
72
+ console.log('\n📖 Parsing CSV file...');
73
+ const data = await parseCSV(options.file);
74
+ console.log(`✅ Parsed ${data.length} records`);
75
+
76
+ // Simple field mapping for people
77
+ const mappedData = data.map((record: any) => {
78
+ const mapped: any = {};
79
+
80
+ // Auto-map common fields for people
81
+ if (options.object === 'people') {
82
+ mapped['name.firstName'] = record.first_name || record.firstName || '';
83
+ mapped['name.lastName'] = record.last_name || record.lastName || '';
84
+ mapped['email'] = record.email || '';
85
+ mapped['phone'] = record.phone || '';
86
+ mapped['jobTitle'] = record.job_title || record.jobTitle || '';
87
+ mapped['city'] = record.city || '';
88
+ mapped['country'] = record.country || '';
89
+ } else {
90
+ // For other objects, just pass through
91
+ Object.assign(mapped, record);
92
+ }
93
+
94
+ return mapped;
95
+ });
96
+
97
+ // Show preview for dry run
98
+ if (options.dryRun) {
99
+ console.log('\n👀 DRY RUN - Preview (first 5 records):');
100
+ console.log(JSON.stringify(mappedData.slice(0, 5), null, 2));
101
+ console.log(`\n📊 Summary: ${mappedData.length} records ready for import`);
102
+ return;
103
+ }
104
+
105
+ // Load data to Twenty
106
+ console.log('\n📤 Loading data to Twenty CRM...');
107
+ const batchSize = parseInt(String(options.batch || '60'));
108
+ const client = axios.create({
109
+ baseURL: `${options.twentyUrl.replace(/\/$/, '')}/rest`,
110
+ headers: {
111
+ 'Authorization': `Bearer ${options.twentyKey}`,
112
+ 'Content-Type': 'application/json'
113
+ },
114
+ timeout: 30000
115
+ });
116
+
117
+ let successCount = 0;
118
+ let errorCount = 0;
119
+
120
+ // Process in batches
121
+ for (let i = 0; i < mappedData.length; i += batchSize) {
122
+ const batch = mappedData.slice(i, i + batchSize);
123
+ const batchNumber = Math.floor(i / batchSize) + 1;
124
+ const totalBatches = Math.ceil(mappedData.length / batchSize);
125
+
126
+ console.log(`📤 Batch ${batchNumber}/${totalBatches}: ${batch.length} records`);
127
+
128
+ try {
129
+ const response = await client.post(`/${options.object}`, {
130
+ data: batch
131
+ });
132
+
133
+ if (response.status === 200) {
134
+ successCount += batch.length;
135
+ console.log(`✅ Batch ${batchNumber} imported successfully`);
136
+ } else {
137
+ throw new Error(`HTTP ${response.status}`);
138
+ }
139
+ } catch (error: any) {
140
+ errorCount += batch.length;
141
+ console.error(`❌ Batch ${batchNumber} failed:`, error.response?.data || error.message);
142
+ }
143
+
144
+ // Small delay to avoid rate limiting
145
+ await new Promise(resolve => setTimeout(resolve, 100));
146
+ }
147
+
148
+ console.log('\n✅ Import completed!');
149
+ console.log(`📊 Success: ${successCount}`);
150
+ console.log(`❌ Errors: ${errorCount}`);
151
+ console.log(`📈 Success rate: ${((successCount / (successCount + errorCount)) * 100).toFixed(2)}%`);
152
+
153
+ } catch (error) {
154
+ console.error('❌ Import failed:', (error as Error).message);
155
+ process.exit(1);
156
+ }
157
+ }
158
+
159
+ main();
@@ -0,0 +1,238 @@
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 TwentyGraphQLClient {
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
+ const query = `
39
+ query {
40
+ people {
41
+ edges {
42
+ node {
43
+ id
44
+ }
45
+ }
46
+ }
47
+ }
48
+ `;
49
+
50
+ const response = await this.client.post('/graphql', { query });
51
+ return response.status === 200 && !response.data.errors;
52
+ } catch (error: any) {
53
+ console.error('❌ Connection test failed:', error.response?.data || error.message);
54
+ return false;
55
+ }
56
+ }
57
+
58
+ async createRecords(objectType: string, records: ParsedRecord[]): Promise<BatchResult> {
59
+ try {
60
+ // Prepare GraphQL mutation based on object type
61
+ let mutation: string;
62
+ let inputType: string;
63
+
64
+ switch (objectType.toLowerCase()) {
65
+ case 'people':
66
+ mutation = `
67
+ mutation CreatePeople($input: [PersonCreateInput!]!) {
68
+ createPeople(input: $input) {
69
+ id
70
+ name {
71
+ firstName
72
+ lastName
73
+ }
74
+ email
75
+ phone
76
+ }
77
+ }
78
+ `;
79
+ inputType = 'PersonCreateInput';
80
+ break;
81
+
82
+ case 'companies':
83
+ mutation = `
84
+ mutation CreateCompanies($input: [CompanyCreateInput!]!) {
85
+ createCompanies(input: $input) {
86
+ id
87
+ name
88
+ domainName
89
+ phone
90
+ }
91
+ }
92
+ `;
93
+ inputType = 'CompanyCreateInput';
94
+ break;
95
+
96
+ default:
97
+ throw new Error(`Unsupported object type: ${objectType}`);
98
+ }
99
+
100
+ // Transform records to GraphQL format
101
+ const graphqlRecords = records.map(record => {
102
+ const transformed: any = {};
103
+
104
+ if (objectType === 'people') {
105
+ // Handle name field
106
+ if (record['name.firstName'] || record['name.lastName']) {
107
+ transformed.name = {
108
+ firstName: record['name.firstName'] || '',
109
+ lastName: record['name.lastName'] || ''
110
+ };
111
+ }
112
+
113
+ // Copy other fields
114
+ if (record.email) transformed.email = record.email;
115
+ if (record.phone) transformed.phone = record.phone;
116
+ if (record.jobTitle) transformed.jobTitle = record.jobTitle;
117
+ if (record.city) transformed.city = record.city;
118
+ if (record.country) transformed.country = record.country;
119
+ }
120
+
121
+ return transformed;
122
+ });
123
+
124
+ const response = await this.client.post('/graphql', {
125
+ query: mutation,
126
+ variables: {
127
+ input: graphqlRecords
128
+ }
129
+ });
130
+
131
+ if (response.data.errors) {
132
+ return {
133
+ success: false,
134
+ count: 0,
135
+ error: response.data.errors.map((e: any) => e.message).join('; ')
136
+ };
137
+ }
138
+
139
+ return {
140
+ success: true,
141
+ count: records.length
142
+ };
143
+ } catch (error: any) {
144
+ return {
145
+ success: false,
146
+ count: 0,
147
+ error: error.response?.data?.message || error.message
148
+ };
149
+ }
150
+ }
151
+
152
+ getObjectEndpoint(objectType: string): string {
153
+ const endpoints: { [key: string]: string } = {
154
+ 'people': 'people',
155
+ 'persons': 'people',
156
+ 'contacts': 'people',
157
+ 'companies': 'companies',
158
+ 'organization': 'companies',
159
+ 'opportunities': 'opportunities',
160
+ 'deals': 'opportunities',
161
+ 'tasks': 'tasks',
162
+ 'notes': 'notes'
163
+ };
164
+
165
+ return endpoints[objectType.toLowerCase()] || objectType;
166
+ }
167
+ }
168
+
169
+ export async function loadToTwenty(
170
+ data: ParsedRecord[],
171
+ objectType: string,
172
+ twentyUrl: string,
173
+ twentyKey: string,
174
+ batchSize: number,
175
+ progressBar: any
176
+ ): Promise<LoadResult> {
177
+ const client = new TwentyGraphQLClient(twentyUrl, twentyKey);
178
+ const errorLog: string[] = [];
179
+ let successCount = 0;
180
+ let errorCount = 0;
181
+
182
+ // Test connection first
183
+ console.log('🔗 Testing Twenty CRM GraphQL connection...');
184
+ const isConnected = await client.testConnection();
185
+ if (!isConnected) {
186
+ throw new Error('Failed to connect to Twenty CRM. Check URL and API key.');
187
+ }
188
+ console.log('✅ GraphQL connection successful');
189
+
190
+ console.log(`📤 Loading ${data.length} records via GraphQL...`);
191
+
192
+ // Process in batches
193
+ for (let i = 0; i < data.length; i += batchSize) {
194
+ const batch = data.slice(i, i + batchSize);
195
+ const batchNumber = Math.floor(i / batchSize) + 1;
196
+ const totalBatches = Math.ceil(data.length / batchSize);
197
+
198
+ try {
199
+ progressBar.update((i / data.length) * 100, {
200
+ batch: `${batchNumber}/${totalBatches}`,
201
+ records: `${i + 1}-${Math.min(i + batchSize, data.length)}`
202
+ });
203
+
204
+ const batchResult = await client.createRecords(objectType, batch);
205
+
206
+ if (batchResult.success) {
207
+ successCount += batchResult.count;
208
+ console.log(`✅ Batch ${batchNumber} imported successfully`);
209
+ } else {
210
+ errorCount += batch.length;
211
+ const errorMsg = `Batch ${batchNumber}: ${batchResult.error}`;
212
+ errorLog.push(errorMsg);
213
+ console.error(`❌ ${errorMsg}`);
214
+ }
215
+ } catch (error: any) {
216
+ const errorMsg = `Batch ${batchNumber}: ${error.message}`;
217
+ errorLog.push(errorMsg);
218
+ errorCount += batch.length;
219
+ }
220
+
221
+ // Small delay to avoid rate limiting
222
+ await new Promise(resolve => setTimeout(resolve, 100));
223
+ }
224
+
225
+ return {
226
+ success: successCount,
227
+ errors: errorCount,
228
+ errorLog
229
+ };
230
+ }
231
+
232
+ export function saveErrorLog(errorLog: string[], filename: string): void {
233
+ if (errorLog.length === 0) return;
234
+
235
+ const logContent = errorLog.join('\n');
236
+ fs.writeFileSync(filename, logContent, 'utf8');
237
+ console.log(`📄 Error log saved to ${filename}`);
238
+ }
@@ -0,0 +1,177 @@
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) {
41
+ console.error('❌ Connection test failed:', (error as any).response?.data || (error as any).message);
42
+ return false;
43
+ }
44
+ }
45
+
46
+ async createRecords(objectType: string, records: ParsedRecord[]): Promise<BatchResult> {
47
+ try {
48
+ const response = await this.client.post(`/${objectType}`, {
49
+ data: records
50
+ });
51
+
52
+ return {
53
+ success: true,
54
+ count: records.length
55
+ };
56
+ } catch (error: any) {
57
+ return {
58
+ success: false,
59
+ count: 0,
60
+ error: error.response?.data?.message || error.message
61
+ };
62
+ }
63
+ }
64
+
65
+ async createRecord(objectType: string, record: ParsedRecord): Promise<BatchResult> {
66
+ try {
67
+ const response = await this.client.post(`/${objectType}`, record);
68
+ return {
69
+ success: true,
70
+ count: 1
71
+ };
72
+ } catch (error: any) {
73
+ return {
74
+ success: false,
75
+ count: 0,
76
+ error: error.response?.data?.message || error.message
77
+ };
78
+ }
79
+ }
80
+
81
+ getObjectEndpoint(objectType: string): string {
82
+ const endpoints: { [key: string]: string } = {
83
+ 'people': 'people',
84
+ 'persons': 'people',
85
+ 'contacts': 'people',
86
+ 'companies': 'companies',
87
+ 'organization': 'companies',
88
+ 'opportunities': 'opportunities',
89
+ 'deals': 'opportunities',
90
+ 'tasks': 'tasks',
91
+ 'notes': 'notes'
92
+ };
93
+
94
+ return endpoints[objectType.toLowerCase()] || objectType;
95
+ }
96
+ }
97
+
98
+ export async function loadToTwenty(
99
+ data: ParsedRecord[],
100
+ objectType: string,
101
+ twentyUrl: string,
102
+ twentyKey: string,
103
+ batchSize: number,
104
+ progressBar: any
105
+ ): Promise<LoadResult> {
106
+ const client = new TwentyAPIClient(twentyUrl, twentyKey);
107
+ const errorLog: string[] = [];
108
+ let successCount = 0;
109
+ let errorCount = 0;
110
+
111
+ // Test connection first
112
+ console.log('🔗 Testing Twenty CRM connection...');
113
+ const isConnected = await client.testConnection();
114
+ if (!isConnected) {
115
+ throw new Error('Failed to connect to Twenty CRM. Check URL and API key.');
116
+ }
117
+ console.log('✅ Connection successful');
118
+
119
+ const endpoint = client.getObjectEndpoint(objectType);
120
+ console.log(`📤 Loading ${data.length} records to ${endpoint}...`);
121
+
122
+ // Process in batches
123
+ for (let i = 0; i < data.length; i += batchSize) {
124
+ const batch = data.slice(i, i + batchSize);
125
+ const batchNumber = Math.floor(i / batchSize) + 1;
126
+ const totalBatches = Math.ceil(data.length / batchSize);
127
+
128
+ try {
129
+ progressBar.update((i / data.length) * 100, {
130
+ batch: `${batchNumber}/${totalBatches}`,
131
+ records: `${i + 1}-${Math.min(i + batchSize, data.length)}`
132
+ });
133
+
134
+ // Try batch create first (if supported)
135
+ const batchResult = await client.createRecords(endpoint, batch);
136
+
137
+ if (batchResult.success) {
138
+ successCount += batchResult.count;
139
+ } else {
140
+ // Fallback to individual records
141
+ console.warn(`⚠️ Batch create failed, trying individual records...`);
142
+
143
+ for (const record of batch) {
144
+ const result = await client.createRecord(endpoint, record);
145
+ if (result.success) {
146
+ successCount++;
147
+ } else {
148
+ errorCount++;
149
+ const errorMsg = `Record ${JSON.stringify(record)}: ${result.error}`;
150
+ errorLog.push(errorMsg);
151
+ }
152
+ }
153
+ }
154
+ } catch (error: any) {
155
+ const errorMsg = `Batch ${batchNumber}: ${error.message}`;
156
+ errorLog.push(errorMsg);
157
+ errorCount += batch.length;
158
+ }
159
+
160
+ // Small delay to avoid rate limiting
161
+ await new Promise(resolve => setTimeout(resolve, 100));
162
+ }
163
+
164
+ return {
165
+ success: successCount,
166
+ errors: errorCount,
167
+ errorLog
168
+ };
169
+ }
170
+
171
+ export function saveErrorLog(errorLog: string[], filename: string): void {
172
+ if (errorLog.length === 0) return;
173
+
174
+ const logContent = errorLog.join('\n');
175
+ fs.writeFileSync(filename, logContent, 'utf8');
176
+ console.log(`📄 Error log saved to ${filename}`);
177
+ }