twenty-migrate-close 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,176 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.transformData = transformData;
4
+ async function transformData(closeData) {
5
+ const transformed = {
6
+ people: [],
7
+ companies: [],
8
+ opportunities: [],
9
+ notes: [],
10
+ activities: []
11
+ };
12
+ console.log('🔄 Transforming Close data to Twenty CRM format...');
13
+ // Transform leads to people and companies
14
+ if (closeData.leads) {
15
+ console.log(`👥 Transforming ${closeData.leads.length} leads...`);
16
+ const { people, companies } = transformLeadsToPeopleAndCompanies(closeData.leads);
17
+ transformed.people = people;
18
+ transformed.companies = companies;
19
+ }
20
+ // Transform opportunities
21
+ if (closeData.opportunities) {
22
+ console.log(`💼 Transforming ${closeData.opportunities.length} opportunities...`);
23
+ transformed.opportunities = transformOpportunities(closeData.opportunities);
24
+ }
25
+ // Transform notes
26
+ if (closeData.notes) {
27
+ console.log(`📝 Transforming ${closeData.notes.length} notes...`);
28
+ transformed.notes = transformNotes(closeData.notes);
29
+ }
30
+ // Transform tasks to activities
31
+ if (closeData.tasks) {
32
+ console.log(`📋 Transforming ${closeData.tasks.length} tasks...`);
33
+ transformed.activities = transformTasks(closeData.tasks);
34
+ }
35
+ console.log('✅ Data transformation completed');
36
+ return transformed;
37
+ }
38
+ function transformLeadsToPeopleAndCompanies(leads) {
39
+ const people = [];
40
+ const companies = [];
41
+ const companyMap = new Map();
42
+ for (const lead of leads) {
43
+ // Transform lead to person
44
+ const person = {
45
+ source: 'close',
46
+ closeId: lead.id,
47
+ createdAt: lead.date_created,
48
+ updatedAt: lead.date_updated
49
+ };
50
+ // Transform name
51
+ if (lead.first_name || lead.last_name) {
52
+ person.name = {
53
+ firstName: lead.first_name || '',
54
+ lastName: lead.last_name || ''
55
+ };
56
+ }
57
+ else if (lead.display_name) {
58
+ // Split full name into first and last name
59
+ const nameParts = lead.display_name.trim().split(/\s+/);
60
+ person.name = {
61
+ firstName: nameParts[0] || '',
62
+ lastName: nameParts.slice(1).join(' ') || ''
63
+ };
64
+ }
65
+ // Transform email
66
+ if (lead.email) {
67
+ person.email = lead.email;
68
+ }
69
+ // Transform phone
70
+ if (lead.phone) {
71
+ person.phone = lead.phone;
72
+ }
73
+ // Transform job title
74
+ if (lead.title) {
75
+ person.jobTitle = lead.title;
76
+ }
77
+ // Transform company name
78
+ if (lead.company_name) {
79
+ person.company = lead.company_name;
80
+ // Create company if not exists
81
+ if (!companyMap.has(lead.company_name)) {
82
+ const company = {
83
+ name: lead.company_name,
84
+ source: 'close',
85
+ closeId: lead.id, // Use lead ID as reference
86
+ createdAt: lead.date_created,
87
+ updatedAt: lead.date_updated
88
+ };
89
+ companyMap.set(lead.company_name, company);
90
+ }
91
+ }
92
+ people.push(person);
93
+ }
94
+ companies.push(...Array.from(companyMap.values()));
95
+ return { people, companies };
96
+ }
97
+ function transformOpportunities(opportunities) {
98
+ return opportunities.map(opportunity => {
99
+ const twentyOpportunity = {
100
+ source: 'close',
101
+ closeId: opportunity.id,
102
+ createdAt: opportunity.date_created,
103
+ updatedAt: opportunity.date_updated
104
+ };
105
+ // Transform name (use status as name if no name provided)
106
+ twentyOpportunity.name = opportunity.status_label || 'Opportunity';
107
+ // Transform amount
108
+ if (opportunity.value !== undefined && opportunity.value !== null) {
109
+ twentyOpportunity.amount = opportunity.value;
110
+ }
111
+ // Transform currency (store as pipeline for now)
112
+ if (opportunity.currency) {
113
+ twentyOpportunity.pipeline = opportunity.currency;
114
+ }
115
+ // Transform status as stage
116
+ if (opportunity.status_label) {
117
+ twentyOpportunity.stage = opportunity.status_label;
118
+ }
119
+ // Transform expected close date
120
+ if (opportunity.expected_close_date) {
121
+ twentyOpportunity.closeDate = opportunity.expected_close_date;
122
+ }
123
+ // Link to lead
124
+ if (opportunity.lead_id) {
125
+ twentyOpportunity.personId = opportunity.lead_id;
126
+ }
127
+ return twentyOpportunity;
128
+ });
129
+ }
130
+ function transformNotes(notes) {
131
+ return notes.map(note => {
132
+ const twentyNote = {
133
+ source: 'close',
134
+ closeId: note.id,
135
+ createdAt: note.date_created,
136
+ updatedAt: note.date_updated
137
+ };
138
+ // Transform note
139
+ if (note.note) {
140
+ twentyNote.body = note.note;
141
+ }
142
+ // Link to lead
143
+ if (note.lead_id) {
144
+ twentyNote.personId = note.lead_id;
145
+ }
146
+ return twentyNote;
147
+ });
148
+ }
149
+ function transformTasks(tasks) {
150
+ return tasks.map(task => {
151
+ const twentyActivity = {
152
+ source: 'close',
153
+ closeId: task.id,
154
+ createdAt: task.date_created,
155
+ updatedAt: task.date_updated
156
+ };
157
+ // Transform text as subject
158
+ if (task.text) {
159
+ twentyActivity.subject = task.text;
160
+ }
161
+ // Transform status as type
162
+ if (task.status) {
163
+ twentyActivity.type = task.status;
164
+ }
165
+ // Transform due date
166
+ if (task.due_date) {
167
+ twentyActivity.note = `Due: ${task.due_date}`;
168
+ }
169
+ // Link to lead
170
+ if (task.lead_id) {
171
+ twentyActivity.personId = task.lead_id;
172
+ }
173
+ return twentyActivity;
174
+ });
175
+ }
176
+ //# 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-close",
3
+ "version": "1.0.0",
4
+ "description": "CLI migration tool for Close to Twenty CRM",
5
+ "main": "dist/cli.js",
6
+ "bin": {
7
+ "twenty-migrate-close": "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
+ "close",
19
+ "migration",
20
+ "cli",
21
+ "data-migration",
22
+ "close-api"
23
+ ],
24
+ "author": "deliveredbyai",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/deliveredbyai/twenty-migrate-close.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/deliveredbyai/twenty-migrate-close/issues"
32
+ },
33
+ "homepage": "https://github.com/deliveredbyai/twenty-migrate-close#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,119 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { extractFromClose } 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
+ apiKey: string;
13
+ twentyUrl: string;
14
+ twentyKey: string;
15
+ dryRun: boolean;
16
+ objects: string;
17
+ batch?: number;
18
+ }
19
+
20
+ const program = new Command();
21
+
22
+ program
23
+ .name('twenty-migrate-close')
24
+ .description('CLI migration tool for Close to Twenty CRM')
25
+ .version('1.0.0');
26
+
27
+ program
28
+ .requiredOption('-k, --api-key <key>', 'Close API key')
29
+ .requiredOption('-u, --twenty-url <url>', 'Twenty CRM URL')
30
+ .requiredOption('-t, --twenty-key <key>', 'Twenty CRM API key')
31
+ .option('-d, --dry-run', 'Preview migration without writing data', false)
32
+ .option('-o, --objects <objects>', 'Objects to migrate (comma-separated)', 'leads,opportunities,notes,tasks')
33
+ .option('-b, --batch <number>', 'Batch size for API calls', '60')
34
+ .parse();
35
+
36
+ const options = program.opts() as Options;
37
+
38
+ async function main() {
39
+ try {
40
+ console.log('🔄 Close to Twenty CRM Migration Tool');
41
+ console.log(`🔗 Close API Key: ${options.apiKey ? '✅ Configured' : '❌ Missing'}`);
42
+ console.log(`🎯 Twenty CRM: ${options.twentyUrl}`);
43
+ console.log(`🔑 Twenty API: ${options.twentyKey ? '✅ Configured' : '❌ Missing'}`);
44
+ console.log(`🧪 Dry Run: ${options.dryRun ? 'YES' : 'NO'}`);
45
+ console.log(`📦 Objects: ${options.objects}`);
46
+
47
+ // Parse objects
48
+ const objects = options.objects.split(',').map(obj => obj.trim().toLowerCase());
49
+
50
+ // Validate objects
51
+ const validObjects = ['leads', 'opportunities', 'notes', 'tasks'];
52
+ const invalidObjects = objects.filter(obj => !validObjects.includes(obj));
53
+
54
+ if (invalidObjects.length > 0) {
55
+ console.error(`❌ Invalid objects: ${invalidObjects.join(', ')}`);
56
+ console.error(`Valid objects: ${validObjects.join(', ')}`);
57
+ process.exit(1);
58
+ }
59
+
60
+ console.log('\n📊 Starting migration...');
61
+ const progressBar = showProgress(0);
62
+
63
+ // Extract data from Close
64
+ console.log('\n📥 Extracting data from Close...');
65
+ const closeData = await extractFromClose(options.apiKey, objects, progressBar);
66
+
67
+ console.log(`✅ Extracted ${Object.keys(closeData).length} object types from Close`);
68
+
69
+ // Show summary for dry run
70
+ if (options.dryRun) {
71
+ console.log('\n👀 DRY RUN - Migration Summary:');
72
+ Object.entries(closeData).forEach(([objectType, data]) => {
73
+ console.log(`📊 ${objectType}: ${data.length} records`);
74
+ });
75
+
76
+ console.log('\n📋 Total records to migrate:');
77
+ const totalRecords = Object.values(closeData).reduce((sum, data) => sum + data.length, 0);
78
+ console.log(`📈 Total: ${totalRecords} records`);
79
+
80
+ progressBar.stop();
81
+ console.log('\n✅ Dry run completed. Use --dry-run=false to execute migration.');
82
+ return;
83
+ }
84
+
85
+ // Transform data
86
+ console.log('\n🔄 Transforming data for Twenty CRM...');
87
+ const transformedData = await transformData(closeData);
88
+
89
+ console.log(`✅ Transformed ${Object.keys(transformedData).length} object types`);
90
+
91
+ // Load data to Twenty CRM
92
+ console.log('\n📤 Loading data to Twenty CRM...');
93
+ const batchSize = parseInt(options.batch?.toString() || '60');
94
+ const result = await loadToTwenty(transformedData, options.twentyUrl, options.twentyKey, batchSize, progressBar);
95
+
96
+ progressBar.stop();
97
+
98
+ // Generate report
99
+ await generateMigrationReport(result, options.objects);
100
+
101
+ console.log('\n✅ Migration completed!');
102
+ console.log(`📊 Success: ${result.success}`);
103
+ console.log(`❌ Errors: ${result.errors}`);
104
+
105
+ if (result.errors > 0) {
106
+ console.log(`📄 Error log: migration-errors-${Date.now()}.log`);
107
+ console.log('\n🔍 Error details:');
108
+ result.errorLog.forEach(error => {
109
+ console.log(` ❌ ${error}`);
110
+ });
111
+ }
112
+
113
+ } catch (error: any) {
114
+ console.error('❌ Migration failed:', error.message);
115
+ process.exit(1);
116
+ }
117
+ }
118
+
119
+ main();
package/src/extract.ts ADDED
@@ -0,0 +1,286 @@
1
+ import axios, { AxiosInstance } from 'axios';
2
+ import { SingleBar } from 'cli-progress';
3
+
4
+ export interface CloseLead {
5
+ id: string;
6
+ display_name?: string;
7
+ first_name?: string;
8
+ last_name?: string;
9
+ email?: string;
10
+ phone?: string;
11
+ company_name?: string;
12
+ title?: string;
13
+ status_label?: string;
14
+ date_created?: string;
15
+ date_updated?: string;
16
+ }
17
+
18
+ export interface CloseOpportunity {
19
+ id: string;
20
+ lead_id?: string;
21
+ status_label?: string;
22
+ value?: number;
23
+ currency?: string;
24
+ confidence?: number;
25
+ date_created?: string;
26
+ date_updated?: string;
27
+ expected_close_date?: string;
28
+ }
29
+
30
+ export interface CloseNote {
31
+ id: string;
32
+ lead_id?: string;
33
+ note?: string;
34
+ date_created?: string;
35
+ date_updated?: string;
36
+ }
37
+
38
+ export interface CloseTask {
39
+ id: string;
40
+ lead_id?: string;
41
+ text?: string;
42
+ status?: string;
43
+ due_date?: string;
44
+ date_created?: string;
45
+ date_updated?: string;
46
+ }
47
+
48
+ export interface CloseData {
49
+ leads: CloseLead[];
50
+ opportunities: CloseOpportunity[];
51
+ notes: CloseNote[];
52
+ tasks: CloseTask[];
53
+ }
54
+
55
+ const RATE_LIMIT_DELAY = 600; // 600ms between requests (100 req/min)
56
+
57
+ export async function extractFromClose(
58
+ apiKey: string,
59
+ objects: string[],
60
+ progressBar: SingleBar
61
+ ): Promise<Partial<CloseData>> {
62
+ const client = createCloseClient(apiKey);
63
+ const data: Partial<CloseData> = {};
64
+
65
+ console.log('🔗 Testing Close API connection...');
66
+
67
+ try {
68
+ // Test connection
69
+ await testCloseConnection(client);
70
+ console.log('✅ Close API connection successful');
71
+ } catch (error: any) {
72
+ console.error('❌ Close API connection failed:', error.message);
73
+ throw new Error('Failed to connect to Close API. Check API key and permissions.');
74
+ }
75
+
76
+ for (const objectType of objects) {
77
+ console.log(`\n📥 Extracting ${objectType}...`);
78
+
79
+ try {
80
+ switch (objectType) {
81
+ case 'leads':
82
+ data.leads = await extractLeads(client, progressBar);
83
+ break;
84
+ case 'opportunities':
85
+ data.opportunities = await extractOpportunities(client, progressBar);
86
+ break;
87
+ case 'notes':
88
+ data.notes = await extractNotes(client, progressBar);
89
+ break;
90
+ case 'tasks':
91
+ data.tasks = await extractTasks(client, progressBar);
92
+ break;
93
+ default:
94
+ console.warn(`⚠️ Unknown object type: ${objectType}`);
95
+ }
96
+ } catch (error: any) {
97
+ console.error(`❌ Failed to extract ${objectType}:`, error.message);
98
+ data[objectType as keyof CloseData] = [];
99
+ }
100
+ }
101
+
102
+ return data;
103
+ }
104
+
105
+ function createCloseClient(apiKey: string): AxiosInstance {
106
+ return axios.create({
107
+ baseURL: 'https://api.close.com/api/v1',
108
+ headers: {
109
+ 'Authorization': `Basic ${Buffer.from(apiKey + ':').toString('base64')}`,
110
+ 'Content-Type': 'application/json'
111
+ },
112
+ timeout: 30000
113
+ });
114
+ }
115
+
116
+ async function testCloseConnection(client: AxiosInstance): Promise<void> {
117
+ try {
118
+ await client.get('/user/me');
119
+ } catch (error: any) {
120
+ throw new Error(`Close API test failed: ${error.message}`);
121
+ }
122
+ }
123
+
124
+ async function extractLeads(client: AxiosInstance, progressBar: SingleBar): Promise<CloseLead[]> {
125
+ const leads: CloseLead[] = [];
126
+ let page = 1;
127
+ const pageSize = 200;
128
+
129
+ while (true) {
130
+ try {
131
+ const response = await client.get('/lead', {
132
+ params: {
133
+ _page: page,
134
+ _limit: pageSize
135
+ }
136
+ });
137
+
138
+ if (response.data && response.data.data && response.data.data.length > 0) {
139
+ leads.push(...response.data.data);
140
+ progressBar.update(leads.length, { leads: leads.length });
141
+
142
+ // Check if there are more records
143
+ if (response.data.data.length < pageSize) {
144
+ break;
145
+ }
146
+
147
+ page++;
148
+ } else {
149
+ break;
150
+ }
151
+
152
+ // Rate limiting
153
+ await new Promise(resolve => setTimeout(resolve, RATE_LIMIT_DELAY));
154
+
155
+ } catch (error: any) {
156
+ console.error(`❌ Error fetching leads page:`, error.message);
157
+ break;
158
+ }
159
+ }
160
+
161
+ console.log(`✅ Extracted ${leads.length} leads`);
162
+ return leads;
163
+ }
164
+
165
+ async function extractOpportunities(client: AxiosInstance, progressBar: SingleBar): Promise<CloseOpportunity[]> {
166
+ const opportunities: CloseOpportunity[] = [];
167
+ let page = 1;
168
+ const pageSize = 200;
169
+
170
+ while (true) {
171
+ try {
172
+ const response = await client.get('/opportunity', {
173
+ params: {
174
+ _page: page,
175
+ _limit: pageSize
176
+ }
177
+ });
178
+
179
+ if (response.data && response.data.data && response.data.data.length > 0) {
180
+ opportunities.push(...response.data.data);
181
+ progressBar.update(opportunities.length, { opportunities: opportunities.length });
182
+
183
+ // Check if there are more records
184
+ if (response.data.data.length < pageSize) {
185
+ break;
186
+ }
187
+
188
+ page++;
189
+ } else {
190
+ break;
191
+ }
192
+
193
+ // Rate limiting
194
+ await new Promise(resolve => setTimeout(resolve, RATE_LIMIT_DELAY));
195
+
196
+ } catch (error: any) {
197
+ console.error(`❌ Error fetching opportunities page:`, error.message);
198
+ break;
199
+ }
200
+ }
201
+
202
+ console.log(`✅ Extracted ${opportunities.length} opportunities`);
203
+ return opportunities;
204
+ }
205
+
206
+ async function extractNotes(client: AxiosInstance, progressBar: SingleBar): Promise<CloseNote[]> {
207
+ const notes: CloseNote[] = [];
208
+ let page = 1;
209
+ const pageSize = 200;
210
+
211
+ while (true) {
212
+ try {
213
+ const response = await client.get('/note', {
214
+ params: {
215
+ _page: page,
216
+ _limit: pageSize
217
+ }
218
+ });
219
+
220
+ if (response.data && response.data.data && response.data.data.length > 0) {
221
+ notes.push(...response.data.data);
222
+ progressBar.update(notes.length, { notes: notes.length });
223
+
224
+ // Check if there are more records
225
+ if (response.data.data.length < pageSize) {
226
+ break;
227
+ }
228
+
229
+ page++;
230
+ } else {
231
+ break;
232
+ }
233
+
234
+ // Rate limiting
235
+ await new Promise(resolve => setTimeout(resolve, RATE_LIMIT_DELAY));
236
+
237
+ } catch (error: any) {
238
+ console.error(`❌ Error fetching notes page:`, error.message);
239
+ break;
240
+ }
241
+ }
242
+
243
+ console.log(`✅ Extracted ${notes.length} notes`);
244
+ return notes;
245
+ }
246
+
247
+ async function extractTasks(client: AxiosInstance, progressBar: SingleBar): Promise<CloseTask[]> {
248
+ const tasks: CloseTask[] = [];
249
+ let page = 1;
250
+ const pageSize = 200;
251
+
252
+ while (true) {
253
+ try {
254
+ const response = await client.get('/task', {
255
+ params: {
256
+ _page: page,
257
+ _limit: pageSize
258
+ }
259
+ });
260
+
261
+ if (response.data && response.data.data && response.data.data.length > 0) {
262
+ tasks.push(...response.data.data);
263
+ progressBar.update(tasks.length, { tasks: tasks.length });
264
+
265
+ // Check if there are more records
266
+ if (response.data.data.length < pageSize) {
267
+ break;
268
+ }
269
+
270
+ page++;
271
+ } else {
272
+ break;
273
+ }
274
+
275
+ // Rate limiting
276
+ await new Promise(resolve => setTimeout(resolve, RATE_LIMIT_DELAY));
277
+
278
+ } catch (error: any) {
279
+ console.error(`❌ Error fetching tasks page:`, error.message);
280
+ break;
281
+ }
282
+ }
283
+
284
+ console.log(`✅ Extracted ${tasks.length} tasks`);
285
+ return tasks;
286
+ }