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,350 @@
1
+ import { chromium, Browser, Page, BrowserContext } from 'playwright';
2
+ import { ParsedRecord } from './parser';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+
6
+ export interface BrowserImportResult {
7
+ success: number;
8
+ errors: number;
9
+ errorLog: string[];
10
+ }
11
+
12
+ export class TwentyBrowserImporter {
13
+ private browser: Browser | null = null;
14
+ private context: BrowserContext | null = null;
15
+ private page: Page | null = null;
16
+ private twentyUrl: string;
17
+
18
+ constructor(twentyUrl: string) {
19
+ this.twentyUrl = twentyUrl.replace(/\/$/, '');
20
+ }
21
+
22
+ async initialize(): Promise<void> {
23
+ console.log('🌐 Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΡ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°...');
24
+
25
+ this.browser = await chromium.launch({
26
+ headless: false, // ΠŸΠΎΠΊΠ°Π·Ρ‹Π²Π°Π΅ΠΌ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€ для ΠΎΡ‚Π»Π°Π΄ΠΊΠΈ
27
+ slowMo: 100 // ЗамСдляСм дСйствия для надСТности
28
+ });
29
+
30
+ this.context = await this.browser.newContext({
31
+ viewport: { width: 1280, height: 720 }
32
+ });
33
+
34
+ this.page = await this.context.newPage();
35
+
36
+ console.log('βœ… Π‘Ρ€Π°ΡƒΠ·Π΅Ρ€ ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½');
37
+ }
38
+
39
+ async login(apiKey: string): Promise<void> {
40
+ if (!this.page) throw new Error('Π‘Ρ€Π°ΡƒΠ·Π΅Ρ€ Π½Π΅ ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½');
41
+
42
+ console.log('πŸ” Π’Ρ…ΠΎΠ΄ Π² Twenty CRM...');
43
+
44
+ await this.page.goto(`${this.twentyUrl}/auth/sign-in`);
45
+ await this.page.waitForLoadState('networkidle');
46
+
47
+ // Π˜Ρ‰Π΅ΠΌ Ρ„ΠΎΡ€ΠΌΡƒ Π²Ρ…ΠΎΠ΄Π°
48
+ await this.page.waitForSelector('input[type="email"], input[name="email"]', { timeout: 10000 });
49
+
50
+ // Если Π΅ΡΡ‚ΡŒ API ΠΊΠ»ΡŽΡ‡, ΠΏΡ€ΠΎΠ±ΡƒΠ΅ΠΌ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Π΅Π³ΠΎ
51
+ if (apiKey) {
52
+ // Π˜Ρ‰Π΅ΠΌ ΠΏΠΎΠ»Π΅ для API ΠΊΠ»ΡŽΡ‡Π° ΠΈΠ»ΠΈ ΡΠΏΠ΅Ρ†ΠΈΠ°Π»ΡŒΠ½Ρ‹ΠΉ Π²Ρ…ΠΎΠ΄
53
+ const apiKeySelector = await this.page.$('input[placeholder*="API"], input[name*="api"], input[type="password"]');
54
+ if (apiKeySelector) {
55
+ await apiKeySelector.fill(apiKey);
56
+ await this.page.click('button[type="submit"]');
57
+ await this.page.waitForLoadState('networkidle');
58
+ console.log('βœ… Π’Ρ…ΠΎΠ΄ Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½ Ρ‡Π΅Ρ€Π΅Π· API ΠΊΠ»ΡŽΡ‡');
59
+ return;
60
+ }
61
+ }
62
+
63
+ console.log('⚠️ ВрСбуСтся Ρ€ΡƒΡ‡Π½ΠΎΠΉ Π²Ρ…ΠΎΠ΄. ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π°, Π²ΠΎΠΉΠ΄ΠΈΡ‚Π΅ Π² систСму...');
64
+ await this.page.waitForURL('**/dashboard', { timeout: 60000 });
65
+ console.log('βœ… Π’Ρ…ΠΎΠ΄ Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½');
66
+ }
67
+
68
+ async navigateToPeople(): Promise<void> {
69
+ if (!this.page) throw new Error('Π‘Ρ€Π°ΡƒΠ·Π΅Ρ€ Π½Π΅ ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½');
70
+
71
+ console.log('πŸ‘₯ ΠŸΠ΅Ρ€Π΅Ρ…ΠΎΠ΄ ΠΊ Ρ€Π°Π·Π΄Π΅Π»Ρƒ People...');
72
+
73
+ await this.page.goto(`${this.twentyUrl}/people`);
74
+ await this.page.waitForLoadState('networkidle');
75
+
76
+ // Π–Π΄Π΅ΠΌ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ Ρ‚Π°Π±Π»ΠΈΡ†Ρ‹ ΠΈΠ»ΠΈ списка
77
+ await this.page.waitForSelector('table, [data-testid="people-list"], .people-container', { timeout: 10000 });
78
+
79
+ console.log('βœ… Π Π°Π·Π΄Π΅Π» People Π·Π°Π³Ρ€ΡƒΠΆΠ΅Π½');
80
+ }
81
+
82
+ async findImportButton(): Promise<boolean> {
83
+ if (!this.page) throw new Error('Π‘Ρ€Π°ΡƒΠ·Π΅Ρ€ Π½Π΅ ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½');
84
+
85
+ console.log('πŸ” Поиск ΠΊΠ½ΠΎΠΏΠΊΠΈ ΠΈΠΌΠΏΠΎΡ€Ρ‚Π°...');
86
+
87
+ const importSelectors = [
88
+ 'button[data-testid="import-button"]',
89
+ 'button[title*="Import"]',
90
+ 'button:has-text("Import")',
91
+ 'a[data-testid="import-link"]',
92
+ 'a[title*="Import"]',
93
+ 'a:has-text("Import")',
94
+ '[data-testid="add-button"]', // Кнопка добавлСния
95
+ 'button:has-text("Add")'
96
+ ];
97
+
98
+ for (const selector of importSelectors) {
99
+ try {
100
+ const element = await this.page.$(selector);
101
+ if (element) {
102
+ console.log(`βœ… НайдСна ΠΊΠ½ΠΎΠΏΠΊΠ° ΠΈΠΌΠΏΠΎΡ€Ρ‚Π°: ${selector}`);
103
+ return true;
104
+ }
105
+ } catch (error) {
106
+ // ΠŸΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠ°Π΅ΠΌ поиск
107
+ }
108
+ }
109
+
110
+ console.log('❌ Кнопка ΠΈΠΌΠΏΠΎΡ€Ρ‚Π° Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Π°');
111
+ return false;
112
+ }
113
+
114
+ async importCSV(csvFilePath: string): Promise<void> {
115
+ if (!this.page) throw new Error('Π‘Ρ€Π°ΡƒΠ·Π΅Ρ€ Π½Π΅ ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½');
116
+
117
+ console.log(`πŸ“ Π˜ΠΌΠΏΠΎΡ€Ρ‚ CSV Ρ„Π°ΠΉΠ»Π°: ${csvFilePath}`);
118
+
119
+ // ΠŸΡ€ΠΎΠ±ΡƒΠ΅ΠΌ Π½Π°ΠΉΡ‚ΠΈ ΠΈ Π½Π°ΠΆΠ°Ρ‚ΡŒ ΠΊΠ½ΠΎΠΏΠΊΡƒ ΠΈΠΌΠΏΠΎΡ€Ρ‚Π°
120
+ const hasImportButton = await this.findImportButton();
121
+
122
+ if (hasImportButton) {
123
+ // НаТимаСм ΠΊΠ½ΠΎΠΏΠΊΡƒ ΠΈΠΌΠΏΠΎΡ€Ρ‚Π°
124
+ await this.page.click('button[data-testid="import-button"], button:has-text("Import"), [data-testid="add-button"]');
125
+ await this.page.waitForTimeout(1000);
126
+
127
+ // Π˜Ρ‰Π΅ΠΌ ΠΏΠΎΠ»Π΅ для Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ Ρ„Π°ΠΉΠ»Π°
128
+ const fileInputSelectors = [
129
+ 'input[type="file"]',
130
+ 'input[accept*=".csv"]',
131
+ '[data-testid="file-input"]'
132
+ ];
133
+
134
+ for (const selector of fileInputSelectors) {
135
+ try {
136
+ const fileInput = await this.page.$(selector);
137
+ if (fileInput) {
138
+ await fileInput.setInputFiles(csvFilePath);
139
+ console.log('βœ… Π€Π°ΠΉΠ» Π·Π°Π³Ρ€ΡƒΠΆΠ΅Π½');
140
+ break;
141
+ }
142
+ } catch (error) {
143
+ // ΠŸΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠ°Π΅ΠΌ поиск
144
+ }
145
+ }
146
+
147
+ // Π˜Ρ‰Π΅ΠΌ ΠΊΠ½ΠΎΠΏΠΊΡƒ подтвСрТдСния ΠΈΠΌΠΏΠΎΡ€Ρ‚Π°
148
+ const submitSelectors = [
149
+ 'button[data-testid="import-submit"]',
150
+ 'button:has-text("Import")',
151
+ 'button:has-text("Upload")',
152
+ 'button[type="submit"]'
153
+ ];
154
+
155
+ for (const selector of submitSelectors) {
156
+ try {
157
+ const submitButton = await this.page.$(selector);
158
+ if (submitButton) {
159
+ await submitButton.click();
160
+ console.log('βœ… Π˜ΠΌΠΏΠΎΡ€Ρ‚ Π·Π°ΠΏΡƒΡ‰Π΅Π½');
161
+ break;
162
+ }
163
+ } catch (error) {
164
+ // ΠŸΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠ°Π΅ΠΌ поиск
165
+ }
166
+ }
167
+
168
+ // Π–Π΄Π΅ΠΌ Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½ΠΈΡ ΠΈΠΌΠΏΠΎΡ€Ρ‚Π°
169
+ await this.page.waitForTimeout(5000);
170
+ } else {
171
+ console.log('❌ АвтоматичСский ΠΈΠΌΠΏΠΎΡ€Ρ‚ Π½Π΅Π²ΠΎΠ·ΠΌΠΎΠΆΠ΅Π½');
172
+ console.log('πŸ’‘ Π Π΅ΠΊΠΎΠΌΠ΅Π½Π΄Π°Ρ†ΠΈΠΈ:');
173
+ console.log(' 1. Π˜ΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΠΉΡ‚Π΅ Π΄Π°Π½Π½Ρ‹Π΅ Π²Ρ€ΡƒΡ‡Π½ΡƒΡŽ Ρ‡Π΅Ρ€Π΅Π· UI');
174
+ console.log(' 2. Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ CSV, ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅ΠΌΡ‹ΠΉ Twenty CRM');
175
+ console.log(' 3. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡŒΡ‚Π΅ ΠΏΡ€Π°Π²Π° доступа для ΠΈΠΌΠΏΠΎΡ€Ρ‚Π°');
176
+ }
177
+ }
178
+
179
+ async importRecords(records: ParsedRecord[]): Promise<BrowserImportResult> {
180
+ const result: BrowserImportResult = {
181
+ success: 0,
182
+ errors: 0,
183
+ errorLog: []
184
+ };
185
+
186
+ if (!this.page) throw new Error('Π‘Ρ€Π°ΡƒΠ·Π΅Ρ€ Π½Π΅ ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½');
187
+
188
+ console.log(`πŸ‘₯ Π˜ΠΌΠΏΠΎΡ€Ρ‚ ${records.length} записСй...`);
189
+
190
+ for (let i = 0; i < records.length; i++) {
191
+ const record = records[i];
192
+
193
+ try {
194
+ console.log(`πŸ“ Π˜ΠΌΠΏΠΎΡ€Ρ‚ записи ${i + 1}/${records.length}`);
195
+
196
+ // ΠŸΠ΅Ρ€Π΅Ρ…ΠΎΠ΄ΠΈΠΌ ΠΊ созданию Π½ΠΎΠ²ΠΎΠΉ записи
197
+ await this.navigateToPeople();
198
+
199
+ // Находим ΠΊΠ½ΠΎΠΏΠΊΡƒ добавлСния
200
+ const addButton = await this.page.$('button[data-testid="add-button"], button:has-text("Add"), button:has-text("New")');
201
+ if (addButton) {
202
+ await addButton.click();
203
+ await this.page.waitForTimeout(1000);
204
+ }
205
+
206
+ // ЗаполняСм Ρ„ΠΎΡ€ΠΌΡƒ
207
+ await this.fillPersonForm(record);
208
+
209
+ // БохраняСм запись
210
+ await this.savePersonForm();
211
+
212
+ result.success++;
213
+ console.log(`βœ… Π—Π°ΠΏΠΈΡΡŒ ${i + 1} ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½Π°`);
214
+
215
+ } catch (error) {
216
+ result.errors++;
217
+ const errorMsg = `Π—Π°ΠΏΠΈΡΡŒ ${i + 1}: ${error}`;
218
+ result.errorLog.push(errorMsg);
219
+ console.error(`❌ ${errorMsg}`);
220
+ }
221
+ }
222
+
223
+ return result;
224
+ }
225
+
226
+ private async fillPersonForm(record: ParsedRecord): Promise<void> {
227
+ if (!this.page) throw new Error('Π‘Ρ€Π°ΡƒΠ·Π΅Ρ€ Π½Π΅ ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½');
228
+
229
+ // ЗаполняСм имя
230
+ if (record['name.firstName']) {
231
+ const firstNameField = await this.page.$('input[name*="firstName"], input[placeholder*="First"], input[data-testid*="first"]');
232
+ if (firstNameField) {
233
+ await firstNameField.fill(record['name.firstName']);
234
+ }
235
+ }
236
+
237
+ if (record['name.lastName']) {
238
+ const lastNameField = await this.page.$('input[name*="lastName"], input[placeholder*="Last"], input[data-testid*="last"]');
239
+ if (lastNameField) {
240
+ await lastNameField.fill(record['name.lastName']);
241
+ }
242
+ }
243
+
244
+ // ЗаполняСм email
245
+ if (record.email) {
246
+ const emailField = await this.page.$('input[name*="email"], input[type="email"], input[placeholder*="Email"]');
247
+ if (emailField) {
248
+ await emailField.fill(record.email);
249
+ }
250
+ }
251
+
252
+ // ЗаполняСм Ρ‚Π΅Π»Π΅Ρ„ΠΎΠ½
253
+ if (record.phone) {
254
+ const phoneField = await this.page.$('input[name*="phone"], input[type="tel"], input[placeholder*="Phone"]');
255
+ if (phoneField) {
256
+ await phoneField.fill(record.phone);
257
+ }
258
+ }
259
+
260
+ // ЗаполняСм Π΄ΠΎΠ»ΠΆΠ½ΠΎΡΡ‚ΡŒ
261
+ if (record.jobTitle) {
262
+ const jobTitleField = await this.page.$('input[name*="jobTitle"], input[placeholder*="Job"], input[placeholder*="Position"]');
263
+ if (jobTitleField) {
264
+ await jobTitleField.fill(record.jobTitle);
265
+ }
266
+ }
267
+ }
268
+
269
+ private async savePersonForm(): Promise<void> {
270
+ if (!this.page) throw new Error('Π‘Ρ€Π°ΡƒΠ·Π΅Ρ€ Π½Π΅ ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½');
271
+
272
+ // Π˜Ρ‰Π΅ΠΌ ΠΊΠ½ΠΎΠΏΠΊΡƒ сохранСния
273
+ const saveSelectors = [
274
+ 'button[data-testid="save-button"]',
275
+ 'button:has-text("Save")',
276
+ 'button:has-text("Create")',
277
+ 'button[type="submit"]'
278
+ ];
279
+
280
+ for (const selector of saveSelectors) {
281
+ try {
282
+ const saveButton = await this.page.$(selector);
283
+ if (saveButton) {
284
+ await saveButton.click();
285
+ await this.page.waitForTimeout(2000);
286
+ break;
287
+ }
288
+ } catch (error) {
289
+ // ΠŸΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠ°Π΅ΠΌ поиск
290
+ }
291
+ }
292
+ }
293
+
294
+ async cleanup(): Promise<void> {
295
+ console.log('🧹 ΠžΡ‡ΠΈΡΡ‚ΠΊΠ° рСсурсов...');
296
+
297
+ if (this.context) {
298
+ await this.context.close();
299
+ }
300
+
301
+ if (this.browser) {
302
+ await this.browser.close();
303
+ }
304
+
305
+ console.log('βœ… РСсурсы ΠΎΡ‡ΠΈΡ‰Π΅Π½Ρ‹');
306
+ }
307
+ }
308
+
309
+ export async function importViaBrowser(
310
+ records: ParsedRecord[],
311
+ objectType: string,
312
+ twentyUrl: string,
313
+ apiKey: string,
314
+ csvFilePath?: string
315
+ ): Promise<BrowserImportResult> {
316
+ const importer = new TwentyBrowserImporter(twentyUrl);
317
+
318
+ try {
319
+ await importer.initialize();
320
+ await importer.login(apiKey);
321
+
322
+ if (csvFilePath) {
323
+ // ΠŸΡ€ΠΎΠ±ΡƒΠ΅ΠΌ ΠΈΠΌΠΏΠΎΡ€Ρ‚ Ρ‡Π΅Ρ€Π΅Π· CSV Ρ„Π°ΠΉΠ»
324
+ await importer.navigateToPeople();
325
+ await importer.importCSV(csvFilePath);
326
+
327
+ return {
328
+ success: records.length,
329
+ errors: 0,
330
+ errorLog: []
331
+ };
332
+ } else {
333
+ // Π˜ΠΌΠΏΠΎΡ€Ρ‚ Ρ‡Π΅Ρ€Π΅Π· Π·Π°ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ Ρ„ΠΎΡ€ΠΌ
334
+ if (objectType === 'people') {
335
+ return await importer.importRecords(records);
336
+ } else {
337
+ throw new Error(`Π’ΠΈΠΏ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Π° ${objectType} ΠΏΠΎΠΊΠ° Π½Π΅ поддСрТиваСтся`);
338
+ }
339
+ }
340
+
341
+ } catch (error) {
342
+ return {
343
+ success: 0,
344
+ errors: records.length,
345
+ errorLog: [`ΠžΠ±Ρ‰Π°Ρ ошибка: ${error}`]
346
+ };
347
+ } finally {
348
+ await importer.cleanup();
349
+ }
350
+ }
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { parseCSV } from './parser';
5
+ import { importViaBrowser } from './browser-automation';
6
+ import { showProgress, generateReport } from './reporter';
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+
10
+ interface Options {
11
+ file: string;
12
+ object: string;
13
+ twentyUrl: string;
14
+ twentyKey: string;
15
+ dryRun: boolean;
16
+ browser: boolean;
17
+ batch?: number;
18
+ }
19
+
20
+ const program = new Command();
21
+
22
+ program
23
+ .name('twenty-import-csv')
24
+ .description('Universal CSV import tool for Twenty CRM (Browser Automation)')
25
+ .version('1.0.0');
26
+
27
+ program
28
+ .requiredOption('-f, --file <path>', 'CSV file to import')
29
+ .requiredOption('-o, --object <type>', 'Twenty object type (people, companies, opportunities, etc.)')
30
+ .requiredOption('-u, --twenty-url <url>', 'Twenty CRM URL')
31
+ .requiredOption('-k, --twenty-key <key>', 'Twenty CRM API key')
32
+ .option('-d, --dry-run', 'Preview import without writing data', false)
33
+ .option('-b, --browser', 'Use browser automation (recommended)', true)
34
+ .option('--batch <number>', 'Batch size for API calls', '60')
35
+ .parse();
36
+
37
+ const options = program.opts() as Options;
38
+
39
+ async function main() {
40
+ try {
41
+ console.log('πŸš€ Twenty CSV Import Tool (Browser Automation)');
42
+ console.log(`πŸ“ File: ${options.file}`);
43
+ console.log(`🎯 Object: ${options.object}`);
44
+ console.log(`πŸ”— Twenty URL: ${options.twentyUrl}`);
45
+ console.log(`🌐 Browser Mode: ${options.browser ? 'YES' : 'NO'}`);
46
+ console.log(`πŸ§ͺ Dry Run: ${options.dryRun ? 'YES' : 'NO'}`);
47
+
48
+ // Check if file exists
49
+ if (!fs.existsSync(options.file)) {
50
+ console.error(`❌ Error: File ${options.file} not found`);
51
+ process.exit(1);
52
+ }
53
+
54
+ // Parse CSV
55
+ console.log('\nπŸ“– Parsing CSV file...');
56
+ const data = await parseCSV(options.file);
57
+ console.log(`βœ… Parsed ${data.length} records`);
58
+
59
+ // Simple field mapping for people
60
+ const mappedData = data.map((record: any) => {
61
+ const mapped: any = {};
62
+
63
+ // Auto-map common fields for people
64
+ if (options.object === 'people') {
65
+ mapped['name.firstName'] = record.first_name || record.firstName || '';
66
+ mapped['name.lastName'] = record.last_name || record.lastName || '';
67
+ mapped['email'] = record.email || '';
68
+ mapped['phone'] = record.phone || '';
69
+ mapped['jobTitle'] = record.job_title || record.jobTitle || '';
70
+ mapped['city'] = record.city || '';
71
+ mapped['country'] = record.country || '';
72
+ } else {
73
+ // For other objects, just pass through
74
+ Object.assign(mapped, record);
75
+ }
76
+
77
+ return mapped;
78
+ });
79
+
80
+ // Show preview for dry run
81
+ if (options.dryRun) {
82
+ console.log('\nπŸ‘€ DRY RUN - Preview (first 5 records):');
83
+ console.log(JSON.stringify(mappedData.slice(0, 5), null, 2));
84
+ console.log(`\nπŸ“Š Summary: ${mappedData.length} records ready for import`);
85
+ console.log('\n🌐 Browser automation will be used for import.');
86
+ return;
87
+ }
88
+
89
+ if (!options.browser) {
90
+ console.log('\n❌ Browser automation is required for Twenty CRM');
91
+ console.log('πŸ’‘ Twenty CRM does not provide a proper REST API');
92
+ console.log('πŸ’‘ Use --browser flag to enable browser automation');
93
+ process.exit(1);
94
+ }
95
+
96
+ // Import data using browser automation
97
+ console.log('\n🌐 Starting browser automation...');
98
+ const progressBar = showProgress(mappedData.length);
99
+
100
+ const result = await importViaBrowser(
101
+ mappedData,
102
+ options.object,
103
+ options.twentyUrl,
104
+ options.twentyKey,
105
+ options.file // Pass CSV file for direct import
106
+ );
107
+
108
+ progressBar.stop();
109
+
110
+ // Generate report
111
+ await generateReport(result, options.object);
112
+
113
+ console.log('\nβœ… Import completed!');
114
+ console.log(`πŸ“Š Success: ${result.success}`);
115
+ console.log(`❌ Errors: ${result.errors}`);
116
+
117
+ if (result.errors > 0) {
118
+ console.log(`πŸ“„ Error log: import-errors-${Date.now()}.log`);
119
+ console.log('\nπŸ” Error details:');
120
+ result.errorLog.forEach(error => {
121
+ console.log(` ❌ ${error}`);
122
+ });
123
+ }
124
+
125
+ console.log('\nπŸ’‘ Browser automation completed!');
126
+ console.log('πŸ’‘ Please check Twenty CRM to verify the imported data.');
127
+
128
+ } catch (error: any) {
129
+ console.error('❌ Import failed:', error.message);
130
+ process.exit(1);
131
+ }
132
+ }
133
+
134
+ main();
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import * as fs from 'fs';
5
+ import * as path from 'path';
6
+ import * as Papa from 'papaparse';
7
+ import axios from 'axios';
8
+
9
+ interface Options {
10
+ file: string;
11
+ object: string;
12
+ twentyUrl: string;
13
+ twentyKey: string;
14
+ dryRun: boolean;
15
+ batch?: number;
16
+ }
17
+
18
+ const program = new Command();
19
+
20
+ program
21
+ .name('twenty-import-csv')
22
+ .description('Universal CSV import tool for Twenty CRM')
23
+ .version('1.0.0');
24
+
25
+ program
26
+ .requiredOption('-f, --file <path>', 'CSV file to import')
27
+ .requiredOption('-o, --object <type>', 'Twenty object type (people, companies, opportunities, etc.)')
28
+ .requiredOption('-u, --twenty-url <url>', 'Twenty CRM URL')
29
+ .requiredOption('-k, --twenty-key <key>', 'Twenty CRM API key')
30
+ .option('-d, --dry-run', 'Preview import without writing data', false)
31
+ .option('-b, --batch <number>', 'Batch size for API calls', '60')
32
+ .parse();
33
+
34
+ const options = program.opts() as Options;
35
+
36
+ async function parseCSV(filePath: string): Promise<any[]> {
37
+ return new Promise((resolve, reject) => {
38
+ const fileContent = fs.readFileSync(filePath, 'utf8');
39
+
40
+ Papa.parse(fileContent, {
41
+ header: true,
42
+ skipEmptyLines: true,
43
+ complete: (results) => {
44
+ if (results.errors.length > 0) {
45
+ console.warn(`⚠️ CSV parsing warnings: ${results.errors.length}`);
46
+ }
47
+ resolve(results.data);
48
+ },
49
+ error: (error: any) => {
50
+ reject(new Error(`CSV parsing failed: ${error.message}`));
51
+ }
52
+ });
53
+ });
54
+ }
55
+
56
+ async function main() {
57
+ try {
58
+ console.log('πŸš€ Twenty CSV Import Tool');
59
+ console.log(`πŸ“ File: ${options.file}`);
60
+ console.log(`🎯 Object: ${options.object}`);
61
+ console.log(`πŸ”— Twenty URL: ${options.twentyUrl}`);
62
+ console.log(`πŸ§ͺ Dry Run: ${options.dryRun ? 'YES' : 'NO'}`);
63
+
64
+ // Check if file exists
65
+ if (!fs.existsSync(options.file)) {
66
+ console.error(`❌ Error: File ${options.file} not found`);
67
+ process.exit(1);
68
+ }
69
+
70
+ // Parse CSV
71
+ console.log('\nπŸ“– Parsing CSV file...');
72
+ const data = await parseCSV(options.file);
73
+ console.log(`βœ… Parsed ${data.length} records`);
74
+
75
+ // Simple field mapping for people
76
+ const mappedData = data.map((record: any) => {
77
+ const mapped: any = {};
78
+
79
+ // Auto-map common fields for people
80
+ if (options.object === 'people') {
81
+ mapped['name.firstName'] = record.first_name || record.firstName || '';
82
+ mapped['name.lastName'] = record.last_name || record.lastName || '';
83
+ mapped['email'] = record.email || '';
84
+ mapped['phone'] = record.phone || '';
85
+ mapped['jobTitle'] = record.job_title || record.jobTitle || '';
86
+ mapped['city'] = record.city || '';
87
+ mapped['country'] = record.country || '';
88
+ } else {
89
+ // For other objects, just pass through
90
+ Object.assign(mapped, record);
91
+ }
92
+
93
+ return mapped;
94
+ });
95
+
96
+ // Show preview for dry run
97
+ if (options.dryRun) {
98
+ console.log('\nπŸ‘€ DRY RUN - Preview (first 5 records):');
99
+ console.log(JSON.stringify(mappedData.slice(0, 5), null, 2));
100
+ console.log(`\nπŸ“Š Summary: ${mappedData.length} records ready for import`);
101
+ return;
102
+ }
103
+
104
+ // Load data to Twenty
105
+ console.log('\nπŸ“€ Loading data to Twenty CRM...');
106
+ const batchSize = parseInt(String(options.batch || '60'));
107
+ const client = axios.create({
108
+ baseURL: `${options.twentyUrl.replace(/\/$/, '')}/rest`,
109
+ headers: {
110
+ 'Authorization': `Bearer ${options.twentyKey}`,
111
+ 'Content-Type': 'application/json'
112
+ },
113
+ timeout: 30000
114
+ });
115
+
116
+ let successCount = 0;
117
+ let errorCount = 0;
118
+
119
+ // Process in batches
120
+ for (let i = 0; i < mappedData.length; i += batchSize) {
121
+ const batch = mappedData.slice(i, i + batchSize);
122
+ const batchNumber = Math.floor(i / batchSize) + 1;
123
+ const totalBatches = Math.ceil(mappedData.length / batchSize);
124
+
125
+ console.log(`πŸ“€ Batch ${batchNumber}/${totalBatches}: ${batch.length} records`);
126
+
127
+ try {
128
+ const response = await client.post(`/${options.object}`, {
129
+ data: batch
130
+ });
131
+
132
+ if (response.status === 200) {
133
+ successCount += batch.length;
134
+ console.log(`βœ… Batch ${batchNumber} imported successfully`);
135
+ } else {
136
+ throw new Error(`HTTP ${response.status}`);
137
+ }
138
+ } catch (error: any) {
139
+ errorCount += batch.length;
140
+ console.error(`❌ Batch ${batchNumber} failed:`, error.response?.data || error.message);
141
+ }
142
+
143
+ // Small delay to avoid rate limiting
144
+ await new Promise(resolve => setTimeout(resolve, 100));
145
+ }
146
+
147
+ console.log('\nβœ… Import completed!');
148
+ console.log(`πŸ“Š Success: ${successCount}`);
149
+ console.log(`❌ Errors: ${errorCount}`);
150
+ console.log(`πŸ“ˆ Success rate: ${((successCount / (successCount + errorCount)) * 100).toFixed(2)}%`);
151
+
152
+ } catch (error) {
153
+ console.error('❌ Import failed:', (error as Error).message);
154
+ process.exit(1);
155
+ }
156
+ }
157
+
158
+ main();