twenty-migrate-espocrm 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.
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.transformData = transformData;
4
+ async function transformData(espocrmData) {
5
+ const transformed = {
6
+ people: [],
7
+ companies: [],
8
+ opportunities: [],
9
+ notes: []
10
+ };
11
+ console.log('🔄 Transforming EspoCRM data to Twenty CRM format...');
12
+ // Transform contacts to people
13
+ if (espocrmData.contacts) {
14
+ console.log(`👥 Transforming ${espocrmData.contacts.length} contacts...`);
15
+ transformed.people = transformContactsToPeople(espocrmData.contacts);
16
+ }
17
+ // Transform accounts
18
+ if (espocrmData.accounts) {
19
+ console.log(`🏢 Transforming ${espocrmData.accounts.length} accounts...`);
20
+ transformed.companies = transformAccounts(espocrmData.accounts);
21
+ }
22
+ // Transform opportunities
23
+ if (espocrmData.opportunities) {
24
+ console.log(`💼 Transforming ${espocrmData.opportunities.length} opportunities...`);
25
+ transformed.opportunities = transformOpportunities(espocrmData.opportunities);
26
+ }
27
+ // Transform notes
28
+ if (espocrmData.notes) {
29
+ console.log(`📝 Transforming ${espocrmData.notes.length} notes...`);
30
+ transformed.notes = transformNotes(espocrmData.notes);
31
+ }
32
+ console.log('✅ Data transformation completed');
33
+ return transformed;
34
+ }
35
+ function transformContactsToPeople(contacts) {
36
+ return contacts.map(contact => {
37
+ const twentyPerson = {
38
+ source: 'espocrm',
39
+ espocrmId: contact.id,
40
+ createdAt: contact.createdAt,
41
+ updatedAt: contact.modifiedAt
42
+ };
43
+ // Transform name
44
+ if (contact.firstName || contact.lastName) {
45
+ twentyPerson.name = {
46
+ firstName: contact.firstName || '',
47
+ lastName: contact.lastName || ''
48
+ };
49
+ }
50
+ // Transform email
51
+ if (contact.email) {
52
+ twentyPerson.email = contact.email;
53
+ }
54
+ // Transform phone
55
+ if (contact.phone) {
56
+ twentyPerson.phone = contact.phone;
57
+ }
58
+ // Transform job title
59
+ if (contact.title) {
60
+ twentyPerson.jobTitle = contact.title;
61
+ }
62
+ // Link to account (stored as company reference)
63
+ if (contact.accountId) {
64
+ twentyPerson.company = contact.accountId;
65
+ }
66
+ return twentyPerson;
67
+ });
68
+ }
69
+ function transformAccounts(accounts) {
70
+ return accounts.map(account => {
71
+ const twentyCompany = {
72
+ source: 'espocrm',
73
+ espocrmId: account.id,
74
+ createdAt: account.createdAt,
75
+ updatedAt: account.modifiedAt
76
+ };
77
+ // Transform name
78
+ if (account.name) {
79
+ twentyCompany.name = account.name;
80
+ }
81
+ // Transform website
82
+ if (account.website) {
83
+ twentyCompany.website = account.website;
84
+ }
85
+ // Transform industry
86
+ if (account.industry) {
87
+ twentyCompany.industry = account.industry;
88
+ }
89
+ // Transform description
90
+ if (account.description) {
91
+ twentyCompany.description = account.description;
92
+ }
93
+ // Transform phone
94
+ if (account.phone) {
95
+ twentyCompany.phone = account.phone;
96
+ }
97
+ return twentyCompany;
98
+ });
99
+ }
100
+ function transformOpportunities(opportunities) {
101
+ return opportunities.map(opportunity => {
102
+ const twentyOpportunity = {
103
+ source: 'espocrm',
104
+ espocrmId: opportunity.id,
105
+ createdAt: opportunity.createdAt,
106
+ updatedAt: opportunity.modifiedAt
107
+ };
108
+ // Transform name
109
+ if (opportunity.name) {
110
+ twentyOpportunity.name = opportunity.name;
111
+ }
112
+ // Transform amount
113
+ if (opportunity.amount !== undefined && opportunity.amount !== null) {
114
+ twentyOpportunity.amount = opportunity.amount;
115
+ }
116
+ // Transform currency (store as pipeline for now)
117
+ if (opportunity.currency) {
118
+ twentyOpportunity.pipeline = opportunity.currency;
119
+ }
120
+ // Transform stage
121
+ if (opportunity.stage) {
122
+ twentyOpportunity.stage = opportunity.stage;
123
+ }
124
+ // Transform close date
125
+ if (opportunity.closeDate) {
126
+ twentyOpportunity.closeDate = opportunity.closeDate;
127
+ }
128
+ // Link to account
129
+ if (opportunity.accountId) {
130
+ twentyOpportunity.companyId = opportunity.accountId;
131
+ }
132
+ // Link to contact
133
+ if (opportunity.contactId) {
134
+ twentyOpportunity.personId = opportunity.contactId;
135
+ }
136
+ return twentyOpportunity;
137
+ });
138
+ }
139
+ function transformNotes(notes) {
140
+ return notes.map(note => {
141
+ const twentyNote = {
142
+ source: 'espocrm',
143
+ espocrmId: note.id,
144
+ createdAt: note.createdAt,
145
+ updatedAt: note.modifiedAt
146
+ };
147
+ // Transform note
148
+ if (note.note) {
149
+ twentyNote.body = note.note;
150
+ }
151
+ // Link to parent based on parentType
152
+ if (note.parentId && note.parentType) {
153
+ switch (note.parentType) {
154
+ case 'Contact':
155
+ twentyNote.personId = note.parentId;
156
+ break;
157
+ case 'Account':
158
+ twentyNote.companyId = note.parentId;
159
+ break;
160
+ case 'Opportunity':
161
+ twentyNote.opportunityId = note.parentId;
162
+ break;
163
+ }
164
+ }
165
+ return twentyNote;
166
+ });
167
+ }
168
+ //# sourceMappingURL=transform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transform.js","sourceRoot":"","sources":["../src/transform.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "twenty-migrate-espocrm",
3
+ "version": "1.0.0",
4
+ "description": "CLI migration tool for EspoCRM to Twenty CRM",
5
+ "main": "dist/cli.js",
6
+ "bin": {
7
+ "twenty-migrate-espocrm": "dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "node dist/cli.js",
12
+ "dev": "tsc --watch",
13
+ "test": "npm run build && node test-migration.js"
14
+ },
15
+ "keywords": [
16
+ "twenty",
17
+ "crm",
18
+ "espocrm",
19
+ "migration",
20
+ "cli",
21
+ "data-migration",
22
+ "espocrm-api"
23
+ ],
24
+ "author": "deliveredbyai",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/deliveredbyai/twenty-migrate-espocrm.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/deliveredbyai/twenty-migrate-espocrm/issues"
32
+ },
33
+ "homepage": "https://github.com/deliveredbyai/twenty-migrate-espocrm#readme",
34
+ "dependencies": {
35
+ "axios": "^1.6.0",
36
+ "cli-progress": "^3.12.0",
37
+ "commander": "^11.1.0",
38
+ "@playwright/test": "^1.40.0",
39
+ "playwright": "^1.40.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/cli-progress": "^3.11.5",
43
+ "@types/node": "^20.10.0",
44
+ "typescript": "^5.3.0"
45
+ },
46
+ "engines": {
47
+ "node": ">=16.0.0"
48
+ }
49
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { extractFromEspoCRM } from './extract';
5
+ import { transformData } from './transform';
6
+ import { loadToTwenty } from './load';
7
+ import { showProgress, generateMigrationReport } from './reporter';
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+
11
+ interface Options {
12
+ url: string;
13
+ apiKey?: string;
14
+ username?: string;
15
+ password?: string;
16
+ twentyUrl: string;
17
+ twentyKey: string;
18
+ dryRun: boolean;
19
+ objects: string;
20
+ batch?: number;
21
+ }
22
+
23
+ const program = new Command();
24
+
25
+ program
26
+ .name('twenty-migrate-espocrm')
27
+ .description('CLI migration tool for EspoCRM to Twenty CRM')
28
+ .version('1.0.0');
29
+
30
+ program
31
+ .requiredOption('-u, --url <url>', 'EspoCRM URL')
32
+ .option('-k, --api-key <key>', 'EspoCRM API key (alternative to username/password)')
33
+ .option('-n, --username <username>', 'EspoCRM username (alternative to API key)')
34
+ .option('-p, --password <password>', 'EspoCRM password (alternative to API key)')
35
+ .requiredOption('-t, --twenty-url <url>', 'Twenty CRM URL')
36
+ .requiredOption('-s, --twenty-key <key>', 'Twenty CRM API key')
37
+ .option('-d, --dry-run', 'Preview migration without writing data', false)
38
+ .option('-o, --objects <objects>', 'Objects to migrate (comma-separated)', 'contacts,accounts,opportunities,notes')
39
+ .option('-b, --batch <number>', 'Batch size for API calls', '60')
40
+ .parse();
41
+
42
+ const options = program.opts() as Options;
43
+
44
+ async function main() {
45
+ try {
46
+ console.log('🔄 EspoCRM to Twenty CRM Migration Tool');
47
+ console.log(`🔗 EspoCRM URL: ${options.url}`);
48
+ console.log(`🔑 EspoCRM Auth: ${options.apiKey ? 'API Key' : options.username ? 'Username/Password' : '❌ Missing'}`);
49
+ console.log(`🎯 Twenty CRM: ${options.twentyUrl}`);
50
+ console.log(`🔑 Twenty API: ${options.twentyKey ? '✅ Configured' : '❌ Missing'}`);
51
+ console.log(`🧪 Dry Run: ${options.dryRun ? 'YES' : 'NO'}`);
52
+ console.log(`📦 Objects: ${options.objects}`);
53
+
54
+ // Validate authentication
55
+ if (!options.apiKey && (!options.username || !options.password)) {
56
+ console.error('❌ Please provide either API key (--api-key) or username and password (--username, --password)');
57
+ process.exit(1);
58
+ }
59
+
60
+ // Parse objects
61
+ const objects = options.objects.split(',').map(obj => obj.trim().toLowerCase());
62
+
63
+ // Validate objects
64
+ const validObjects = ['contacts', 'accounts', 'opportunities', 'notes'];
65
+ const invalidObjects = objects.filter(obj => !validObjects.includes(obj));
66
+
67
+ if (invalidObjects.length > 0) {
68
+ console.error(`❌ Invalid objects: ${invalidObjects.join(', ')}`);
69
+ console.error(`Valid objects: ${validObjects.join(', ')}`);
70
+ process.exit(1);
71
+ }
72
+
73
+ console.log('\n📊 Starting migration...');
74
+ const progressBar = showProgress(0);
75
+
76
+ // Extract data from EspoCRM
77
+ console.log('\n📥 Extracting data from EspoCRM...');
78
+ const espocrmData = await extractFromEspoCRM(
79
+ options.url,
80
+ options.apiKey,
81
+ options.username,
82
+ options.password,
83
+ objects,
84
+ progressBar
85
+ );
86
+
87
+ console.log(`✅ Extracted ${Object.keys(espocrmData).length} object types from EspoCRM`);
88
+
89
+ // Show summary for dry run
90
+ if (options.dryRun) {
91
+ console.log('\n👀 DRY RUN - Migration Summary:');
92
+ Object.entries(espocrmData).forEach(([objectType, data]) => {
93
+ console.log(`📊 ${objectType}: ${data.length} records`);
94
+ });
95
+
96
+ console.log('\n📋 Total records to migrate:');
97
+ const totalRecords = Object.values(espocrmData).reduce((sum, data) => sum + data.length, 0);
98
+ console.log(`📈 Total: ${totalRecords} records`);
99
+
100
+ progressBar.stop();
101
+ console.log('\n✅ Dry run completed. Use --dry-run=false to execute migration.');
102
+ return;
103
+ }
104
+
105
+ // Transform data
106
+ console.log('\n🔄 Transforming data for Twenty CRM...');
107
+ const transformedData = await transformData(espocrmData);
108
+
109
+ console.log(`✅ Transformed ${Object.keys(transformedData).length} object types`);
110
+
111
+ // Load data to Twenty CRM
112
+ console.log('\n📤 Loading data to Twenty CRM...');
113
+ const batchSize = parseInt(options.batch?.toString() || '60');
114
+ const result = await loadToTwenty(transformedData, options.twentyUrl, options.twentyKey, batchSize, progressBar);
115
+
116
+ progressBar.stop();
117
+
118
+ // Generate report
119
+ await generateMigrationReport(result, options.objects);
120
+
121
+ console.log('\n✅ Migration completed!');
122
+ console.log(`📊 Success: ${result.success}`);
123
+ console.log(`❌ Errors: ${result.errors}`);
124
+
125
+ if (result.errors > 0) {
126
+ console.log(`📄 Error log: migration-errors-${Date.now()}.log`);
127
+ console.log('\n🔍 Error details:');
128
+ result.errorLog.forEach(error => {
129
+ console.log(` ❌ ${error}`);
130
+ });
131
+ }
132
+
133
+ } catch (error: any) {
134
+ console.error('❌ Migration failed:', error.message);
135
+ process.exit(1);
136
+ }
137
+ }
138
+
139
+ main();
package/src/extract.ts ADDED
@@ -0,0 +1,309 @@
1
+ import axios, { AxiosInstance } from 'axios';
2
+ import { SingleBar } from 'cli-progress';
3
+
4
+ export interface EspoCRMContact {
5
+ id: string;
6
+ firstName?: string;
7
+ lastName?: string;
8
+ email?: string;
9
+ phone?: string;
10
+ accountId?: string;
11
+ title?: string;
12
+ createdAt?: string;
13
+ modifiedAt?: string;
14
+ }
15
+
16
+ export interface EspoCRMAccount {
17
+ id: string;
18
+ name?: string;
19
+ website?: string;
20
+ industry?: string;
21
+ phone?: string;
22
+ description?: string;
23
+ createdAt?: string;
24
+ modifiedAt?: string;
25
+ }
26
+
27
+ export interface EspoCRMOpportunity {
28
+ id: string;
29
+ name?: string;
30
+ amount?: number;
31
+ currency?: string;
32
+ stage?: string;
33
+ accountId?: string;
34
+ contactId?: string;
35
+ closeDate?: string;
36
+ createdAt?: string;
37
+ modifiedAt?: string;
38
+ }
39
+
40
+ export interface EspoCRMNote {
41
+ id: string;
42
+ note?: string;
43
+ parentId?: string;
44
+ parentType?: string;
45
+ createdAt?: string;
46
+ modifiedAt?: string;
47
+ }
48
+
49
+ export interface EspoCRMData {
50
+ contacts: EspoCRMContact[];
51
+ accounts: EspoCRMAccount[];
52
+ opportunities: EspoCRMOpportunity[];
53
+ notes: EspoCRMNote[];
54
+ }
55
+
56
+ const RATE_LIMIT_DELAY = 600; // 600ms between requests (100 req/min)
57
+
58
+ export async function extractFromEspoCRM(
59
+ url: string,
60
+ apiKey: string | undefined,
61
+ username: string | undefined,
62
+ password: string | undefined,
63
+ objects: string[],
64
+ progressBar: SingleBar
65
+ ): Promise<Partial<EspoCRMData>> {
66
+ const client = await createEspoCRMClient(url, apiKey, username, password);
67
+ const data: Partial<EspoCRMData> = {};
68
+
69
+ console.log('🔗 Testing EspoCRM API connection...');
70
+
71
+ try {
72
+ // Test connection
73
+ await testEspoCRMConnection(client);
74
+ console.log('✅ EspoCRM API connection successful');
75
+ } catch (error: any) {
76
+ console.error('❌ EspoCRM API connection failed:', error.message);
77
+ throw new Error('Failed to connect to EspoCRM API. Check credentials and permissions.');
78
+ }
79
+
80
+ for (const objectType of objects) {
81
+ console.log(`\n📥 Extracting ${objectType}...`);
82
+
83
+ try {
84
+ switch (objectType) {
85
+ case 'contacts':
86
+ data.contacts = await extractContacts(client, progressBar);
87
+ break;
88
+ case 'accounts':
89
+ data.accounts = await extractAccounts(client, progressBar);
90
+ break;
91
+ case 'opportunities':
92
+ data.opportunities = await extractOpportunities(client, progressBar);
93
+ break;
94
+ case 'notes':
95
+ data.notes = await extractNotes(client, progressBar);
96
+ break;
97
+ default:
98
+ console.warn(`⚠️ Unknown object type: ${objectType}`);
99
+ }
100
+ } catch (error: any) {
101
+ console.error(`❌ Failed to extract ${objectType}:`, error.message);
102
+ data[objectType as keyof EspoCRMData] = [];
103
+ }
104
+ }
105
+
106
+ return data;
107
+ }
108
+
109
+ async function createEspoCRMClient(
110
+ url: string,
111
+ apiKey?: string,
112
+ username?: string,
113
+ password?: string
114
+ ): Promise<AxiosInstance> {
115
+ const client = axios.create({
116
+ baseURL: `${url}/api/v1`,
117
+ timeout: 30000
118
+ });
119
+
120
+ // Set up authentication
121
+ if (apiKey) {
122
+ // API Key authentication
123
+ client.defaults.headers.common['Api-Key'] = apiKey;
124
+ } else if (username && password) {
125
+ // Basic authentication
126
+ const authString = Buffer.from(`${username}:${password}`).toString('base64');
127
+ client.defaults.headers.common['Authorization'] = `Basic ${authString}`;
128
+ } else {
129
+ throw new Error('Either API key or username/password must be provided');
130
+ }
131
+
132
+ return client;
133
+ }
134
+
135
+ async function testEspoCRMConnection(client: AxiosInstance): Promise<void> {
136
+ try {
137
+ await client.get('/App/user');
138
+ } catch (error: any) {
139
+ throw new Error(`EspoCRM API test failed: ${error.message}`);
140
+ }
141
+ }
142
+
143
+ async function extractContacts(client: AxiosInstance, progressBar: SingleBar): Promise<EspoCRMContact[]> {
144
+ const contacts: EspoCRMContact[] = [];
145
+ let offset = 0;
146
+ const maxSize = 200;
147
+
148
+ while (true) {
149
+ try {
150
+ const response = await client.get('/Contact', {
151
+ params: {
152
+ offset,
153
+ maxSize,
154
+ select: 'id,firstName,lastName,email,phone,accountId,title,createdAt,modifiedAt'
155
+ }
156
+ });
157
+
158
+ if (response.data && response.data.list && response.data.list.length > 0) {
159
+ contacts.push(...response.data.list);
160
+ progressBar.update(contacts.length, { contacts: contacts.length });
161
+
162
+ // Check if there are more records
163
+ if (response.data.list.length < maxSize) {
164
+ break;
165
+ }
166
+
167
+ offset += maxSize;
168
+ } else {
169
+ break;
170
+ }
171
+
172
+ // Rate limiting
173
+ await new Promise(resolve => setTimeout(resolve, RATE_LIMIT_DELAY));
174
+
175
+ } catch (error: any) {
176
+ console.error(`❌ Error fetching contacts:`, error.message);
177
+ break;
178
+ }
179
+ }
180
+
181
+ console.log(`✅ Extracted ${contacts.length} contacts`);
182
+ return contacts;
183
+ }
184
+
185
+ async function extractAccounts(client: AxiosInstance, progressBar: SingleBar): Promise<EspoCRMAccount[]> {
186
+ const accounts: EspoCRMAccount[] = [];
187
+ let offset = 0;
188
+ const maxSize = 200;
189
+
190
+ while (true) {
191
+ try {
192
+ const response = await client.get('/Account', {
193
+ params: {
194
+ offset,
195
+ maxSize,
196
+ select: 'id,name,website,industry,phone,description,createdAt,modifiedAt'
197
+ }
198
+ });
199
+
200
+ if (response.data && response.data.list && response.data.list.length > 0) {
201
+ accounts.push(...response.data.list);
202
+ progressBar.update(accounts.length, { accounts: accounts.length });
203
+
204
+ // Check if there are more records
205
+ if (response.data.list.length < maxSize) {
206
+ break;
207
+ }
208
+
209
+ offset += maxSize;
210
+ } else {
211
+ break;
212
+ }
213
+
214
+ // Rate limiting
215
+ await new Promise(resolve => setTimeout(resolve, RATE_LIMIT_DELAY));
216
+
217
+ } catch (error: any) {
218
+ console.error(`❌ Error fetching accounts:`, error.message);
219
+ break;
220
+ }
221
+ }
222
+
223
+ console.log(`✅ Extracted ${accounts.length} accounts`);
224
+ return accounts;
225
+ }
226
+
227
+ async function extractOpportunities(client: AxiosInstance, progressBar: SingleBar): Promise<EspoCRMOpportunity[]> {
228
+ const opportunities: EspoCRMOpportunity[] = [];
229
+ let offset = 0;
230
+ const maxSize = 200;
231
+
232
+ while (true) {
233
+ try {
234
+ const response = await client.get('/Opportunity', {
235
+ params: {
236
+ offset,
237
+ maxSize,
238
+ select: 'id,name,amount,currency,stage,accountId,contactId,closeDate,createdAt,modifiedAt'
239
+ }
240
+ });
241
+
242
+ if (response.data && response.data.list && response.data.list.length > 0) {
243
+ opportunities.push(...response.data.list);
244
+ progressBar.update(opportunities.length, { opportunities: opportunities.length });
245
+
246
+ // Check if there are more records
247
+ if (response.data.list.length < maxSize) {
248
+ break;
249
+ }
250
+
251
+ offset += maxSize;
252
+ } else {
253
+ break;
254
+ }
255
+
256
+ // Rate limiting
257
+ await new Promise(resolve => setTimeout(resolve, RATE_LIMIT_DELAY));
258
+
259
+ } catch (error: any) {
260
+ console.error(`❌ Error fetching opportunities:`, error.message);
261
+ break;
262
+ }
263
+ }
264
+
265
+ console.log(`✅ Extracted ${opportunities.length} opportunities`);
266
+ return opportunities;
267
+ }
268
+
269
+ async function extractNotes(client: AxiosInstance, progressBar: SingleBar): Promise<EspoCRMNote[]> {
270
+ const notes: EspoCRMNote[] = [];
271
+ let offset = 0;
272
+ const maxSize = 200;
273
+
274
+ while (true) {
275
+ try {
276
+ const response = await client.get('/Note', {
277
+ params: {
278
+ offset,
279
+ maxSize,
280
+ select: 'id,note,parentId,parentType,createdAt,modifiedAt'
281
+ }
282
+ });
283
+
284
+ if (response.data && response.data.list && response.data.list.length > 0) {
285
+ notes.push(...response.data.list);
286
+ progressBar.update(notes.length, { notes: notes.length });
287
+
288
+ // Check if there are more records
289
+ if (response.data.list.length < maxSize) {
290
+ break;
291
+ }
292
+
293
+ offset += maxSize;
294
+ } else {
295
+ break;
296
+ }
297
+
298
+ // Rate limiting
299
+ await new Promise(resolve => setTimeout(resolve, RATE_LIMIT_DELAY));
300
+
301
+ } catch (error: any) {
302
+ console.error(`❌ Error fetching notes:`, error.message);
303
+ break;
304
+ }
305
+ }
306
+
307
+ console.log(`✅ Extracted ${notes.length} notes`);
308
+ return notes;
309
+ }