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.
- package/.env.example +13 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +88 -0
- package/dist/cli.js.map +1 -0
- package/dist/extract.d.ts +49 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +207 -0
- package/dist/extract.js.map +1 -0
- package/dist/load.d.ts +50 -0
- package/dist/load.d.ts.map +1 -0
- package/dist/load.js +280 -0
- package/dist/load.js.map +1 -0
- package/dist/reporter.d.ts +6 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +212 -0
- package/dist/reporter.js.map +1 -0
- package/dist/transform.d.ts +73 -0
- package/dist/transform.d.ts.map +1 -0
- package/dist/transform.js +176 -0
- package/dist/transform.js.map +1 -0
- package/package.json +49 -0
- package/src/cli.ts +119 -0
- package/src/extract.ts +286 -0
- package/src/load.ts +293 -0
- package/src/reporter.ts +203 -0
- package/src/transform.ts +284 -0
- package/tsconfig.json +39 -0
package/src/transform.ts
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { CloseData, CloseLead, CloseOpportunity, CloseNote, CloseTask } from './extract';
|
|
2
|
+
|
|
3
|
+
export interface TwentyPerson {
|
|
4
|
+
name?: {
|
|
5
|
+
firstName?: string;
|
|
6
|
+
lastName?: string;
|
|
7
|
+
};
|
|
8
|
+
email?: string;
|
|
9
|
+
phone?: string;
|
|
10
|
+
jobTitle?: string;
|
|
11
|
+
company?: string;
|
|
12
|
+
city?: string;
|
|
13
|
+
country?: string;
|
|
14
|
+
createdAt?: string;
|
|
15
|
+
updatedAt?: string;
|
|
16
|
+
source?: 'close';
|
|
17
|
+
closeId?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TwentyCompany {
|
|
21
|
+
name?: string;
|
|
22
|
+
domainName?: string;
|
|
23
|
+
industry?: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
phone?: string;
|
|
26
|
+
website?: string;
|
|
27
|
+
createdAt?: string;
|
|
28
|
+
updatedAt?: string;
|
|
29
|
+
source?: 'close';
|
|
30
|
+
closeId?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TwentyOpportunity {
|
|
34
|
+
name?: string;
|
|
35
|
+
amount?: number;
|
|
36
|
+
closeDate?: string;
|
|
37
|
+
pipeline?: string;
|
|
38
|
+
stage?: string;
|
|
39
|
+
createdAt?: string;
|
|
40
|
+
updatedAt?: string;
|
|
41
|
+
source?: 'close';
|
|
42
|
+
closeId?: string;
|
|
43
|
+
personId?: string;
|
|
44
|
+
companyId?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface TwentyNote {
|
|
48
|
+
body?: string;
|
|
49
|
+
createdAt?: string;
|
|
50
|
+
updatedAt?: string;
|
|
51
|
+
source?: 'close';
|
|
52
|
+
closeId?: string;
|
|
53
|
+
personId?: string;
|
|
54
|
+
companyId?: string;
|
|
55
|
+
opportunityId?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface TwentyActivity {
|
|
59
|
+
subject?: string;
|
|
60
|
+
note?: string;
|
|
61
|
+
type?: string;
|
|
62
|
+
createdAt?: string;
|
|
63
|
+
updatedAt?: string;
|
|
64
|
+
source?: 'close';
|
|
65
|
+
closeId?: string;
|
|
66
|
+
personId?: string;
|
|
67
|
+
companyId?: string;
|
|
68
|
+
opportunityId?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface TransformedData {
|
|
72
|
+
people: TwentyPerson[];
|
|
73
|
+
companies: TwentyCompany[];
|
|
74
|
+
opportunities: TwentyOpportunity[];
|
|
75
|
+
notes: TwentyNote[];
|
|
76
|
+
activities: TwentyActivity[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function transformData(closeData: Partial<CloseData>): Promise<TransformedData> {
|
|
80
|
+
const transformed: TransformedData = {
|
|
81
|
+
people: [],
|
|
82
|
+
companies: [],
|
|
83
|
+
opportunities: [],
|
|
84
|
+
notes: [],
|
|
85
|
+
activities: []
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
console.log('🔄 Transforming Close data to Twenty CRM format...');
|
|
89
|
+
|
|
90
|
+
// Transform leads to people and companies
|
|
91
|
+
if (closeData.leads) {
|
|
92
|
+
console.log(`👥 Transforming ${closeData.leads.length} leads...`);
|
|
93
|
+
const { people, companies } = transformLeadsToPeopleAndCompanies(closeData.leads);
|
|
94
|
+
transformed.people = people;
|
|
95
|
+
transformed.companies = companies;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Transform opportunities
|
|
99
|
+
if (closeData.opportunities) {
|
|
100
|
+
console.log(`💼 Transforming ${closeData.opportunities.length} opportunities...`);
|
|
101
|
+
transformed.opportunities = transformOpportunities(closeData.opportunities);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Transform notes
|
|
105
|
+
if (closeData.notes) {
|
|
106
|
+
console.log(`📝 Transforming ${closeData.notes.length} notes...`);
|
|
107
|
+
transformed.notes = transformNotes(closeData.notes);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Transform tasks to activities
|
|
111
|
+
if (closeData.tasks) {
|
|
112
|
+
console.log(`📋 Transforming ${closeData.tasks.length} tasks...`);
|
|
113
|
+
transformed.activities = transformTasks(closeData.tasks);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log('✅ Data transformation completed');
|
|
117
|
+
return transformed;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function transformLeadsToPeopleAndCompanies(leads: CloseLead[]): { people: TwentyPerson[], companies: TwentyCompany[] } {
|
|
121
|
+
const people: TwentyPerson[] = [];
|
|
122
|
+
const companies: TwentyCompany[] = [];
|
|
123
|
+
const companyMap = new Map<string, TwentyCompany>();
|
|
124
|
+
|
|
125
|
+
for (const lead of leads) {
|
|
126
|
+
// Transform lead to person
|
|
127
|
+
const person: TwentyPerson = {
|
|
128
|
+
source: 'close',
|
|
129
|
+
closeId: lead.id,
|
|
130
|
+
createdAt: lead.date_created,
|
|
131
|
+
updatedAt: lead.date_updated
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Transform name
|
|
135
|
+
if (lead.first_name || lead.last_name) {
|
|
136
|
+
person.name = {
|
|
137
|
+
firstName: lead.first_name || '',
|
|
138
|
+
lastName: lead.last_name || ''
|
|
139
|
+
};
|
|
140
|
+
} else if (lead.display_name) {
|
|
141
|
+
// Split full name into first and last name
|
|
142
|
+
const nameParts = lead.display_name.trim().split(/\s+/);
|
|
143
|
+
person.name = {
|
|
144
|
+
firstName: nameParts[0] || '',
|
|
145
|
+
lastName: nameParts.slice(1).join(' ') || ''
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Transform email
|
|
150
|
+
if (lead.email) {
|
|
151
|
+
person.email = lead.email;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Transform phone
|
|
155
|
+
if (lead.phone) {
|
|
156
|
+
person.phone = lead.phone;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Transform job title
|
|
160
|
+
if (lead.title) {
|
|
161
|
+
person.jobTitle = lead.title;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Transform company name
|
|
165
|
+
if (lead.company_name) {
|
|
166
|
+
person.company = lead.company_name;
|
|
167
|
+
|
|
168
|
+
// Create company if not exists
|
|
169
|
+
if (!companyMap.has(lead.company_name)) {
|
|
170
|
+
const company: TwentyCompany = {
|
|
171
|
+
name: lead.company_name,
|
|
172
|
+
source: 'close',
|
|
173
|
+
closeId: lead.id, // Use lead ID as reference
|
|
174
|
+
createdAt: lead.date_created,
|
|
175
|
+
updatedAt: lead.date_updated
|
|
176
|
+
};
|
|
177
|
+
companyMap.set(lead.company_name, company);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
people.push(person);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
companies.push(...Array.from(companyMap.values()));
|
|
185
|
+
|
|
186
|
+
return { people, companies };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function transformOpportunities(opportunities: CloseOpportunity[]): TwentyOpportunity[] {
|
|
190
|
+
return opportunities.map(opportunity => {
|
|
191
|
+
const twentyOpportunity: TwentyOpportunity = {
|
|
192
|
+
source: 'close',
|
|
193
|
+
closeId: opportunity.id,
|
|
194
|
+
createdAt: opportunity.date_created,
|
|
195
|
+
updatedAt: opportunity.date_updated
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Transform name (use status as name if no name provided)
|
|
199
|
+
twentyOpportunity.name = opportunity.status_label || 'Opportunity';
|
|
200
|
+
|
|
201
|
+
// Transform amount
|
|
202
|
+
if (opportunity.value !== undefined && opportunity.value !== null) {
|
|
203
|
+
twentyOpportunity.amount = opportunity.value;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Transform currency (store as pipeline for now)
|
|
207
|
+
if (opportunity.currency) {
|
|
208
|
+
twentyOpportunity.pipeline = opportunity.currency;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Transform status as stage
|
|
212
|
+
if (opportunity.status_label) {
|
|
213
|
+
twentyOpportunity.stage = opportunity.status_label;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Transform expected close date
|
|
217
|
+
if (opportunity.expected_close_date) {
|
|
218
|
+
twentyOpportunity.closeDate = opportunity.expected_close_date;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Link to lead
|
|
222
|
+
if (opportunity.lead_id) {
|
|
223
|
+
twentyOpportunity.personId = opportunity.lead_id;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return twentyOpportunity;
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function transformNotes(notes: CloseNote[]): TwentyNote[] {
|
|
231
|
+
return notes.map(note => {
|
|
232
|
+
const twentyNote: TwentyNote = {
|
|
233
|
+
source: 'close',
|
|
234
|
+
closeId: note.id,
|
|
235
|
+
createdAt: note.date_created,
|
|
236
|
+
updatedAt: note.date_updated
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// Transform note
|
|
240
|
+
if (note.note) {
|
|
241
|
+
twentyNote.body = note.note;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Link to lead
|
|
245
|
+
if (note.lead_id) {
|
|
246
|
+
twentyNote.personId = note.lead_id;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return twentyNote;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function transformTasks(tasks: CloseTask[]): TwentyActivity[] {
|
|
254
|
+
return tasks.map(task => {
|
|
255
|
+
const twentyActivity: TwentyActivity = {
|
|
256
|
+
source: 'close',
|
|
257
|
+
closeId: task.id,
|
|
258
|
+
createdAt: task.date_created,
|
|
259
|
+
updatedAt: task.date_updated
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// Transform text as subject
|
|
263
|
+
if (task.text) {
|
|
264
|
+
twentyActivity.subject = task.text;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Transform status as type
|
|
268
|
+
if (task.status) {
|
|
269
|
+
twentyActivity.type = task.status;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Transform due date
|
|
273
|
+
if (task.due_date) {
|
|
274
|
+
twentyActivity.note = `Due: ${task.due_date}`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Link to lead
|
|
278
|
+
if (task.lead_id) {
|
|
279
|
+
twentyActivity.personId = task.lead_id;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return twentyActivity;
|
|
283
|
+
});
|
|
284
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"removeComments": false,
|
|
16
|
+
"noImplicitAny": true,
|
|
17
|
+
"strictNullChecks": true,
|
|
18
|
+
"strictFunctionTypes": true,
|
|
19
|
+
"noImplicitReturns": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"moduleResolution": "node",
|
|
22
|
+
"baseUrl": "./",
|
|
23
|
+
"paths": {
|
|
24
|
+
"@/*": ["src/*"]
|
|
25
|
+
},
|
|
26
|
+
"allowSyntheticDefaultImports": true,
|
|
27
|
+
"experimentalDecorators": true,
|
|
28
|
+
"emitDecoratorMetadata": true
|
|
29
|
+
},
|
|
30
|
+
"include": [
|
|
31
|
+
"src/**/*"
|
|
32
|
+
],
|
|
33
|
+
"exclude": [
|
|
34
|
+
"node_modules",
|
|
35
|
+
"dist",
|
|
36
|
+
"**/*.test.ts",
|
|
37
|
+
"**/*.spec.ts"
|
|
38
|
+
]
|
|
39
|
+
}
|