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/load.ts
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import axios, { AxiosInstance } from 'axios';
|
|
2
|
+
import { TransformedData, TwentyPerson, TwentyCompany, TwentyOpportunity, TwentyNote, TwentyActivity } from './transform';
|
|
3
|
+
import { SingleBar } from 'cli-progress';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
|
|
6
|
+
export interface LoadResult {
|
|
7
|
+
success: number;
|
|
8
|
+
errors: number;
|
|
9
|
+
errorLog: string[];
|
|
10
|
+
objectResults: {
|
|
11
|
+
people: { success: number; errors: number };
|
|
12
|
+
companies: { success: number; errors: number };
|
|
13
|
+
opportunities: { success: number; errors: number };
|
|
14
|
+
notes: { success: number; errors: number };
|
|
15
|
+
activities: { success: number; errors: number };
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface BatchResult {
|
|
20
|
+
success: boolean;
|
|
21
|
+
count: number;
|
|
22
|
+
error?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class TwentyAPIClient {
|
|
26
|
+
private client: AxiosInstance;
|
|
27
|
+
private baseUrl: string;
|
|
28
|
+
private apiKey: string;
|
|
29
|
+
private existingEmails: Set<string> = new Set();
|
|
30
|
+
private existingCompanies: Set<string> = new Set();
|
|
31
|
+
|
|
32
|
+
constructor(baseUrl: string, apiKey: string) {
|
|
33
|
+
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
34
|
+
this.apiKey = apiKey;
|
|
35
|
+
|
|
36
|
+
this.client = axios.create({
|
|
37
|
+
baseURL: `${this.baseUrl}`,
|
|
38
|
+
headers: {
|
|
39
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
40
|
+
'Content-Type': 'application/json'
|
|
41
|
+
},
|
|
42
|
+
timeout: 30000
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async testConnection(): Promise<boolean> {
|
|
47
|
+
try {
|
|
48
|
+
const response = await this.client.get('/api/ping');
|
|
49
|
+
return response.status === 200;
|
|
50
|
+
} catch (error: any) {
|
|
51
|
+
console.error('ā Connection test failed:', error.response?.data || error.message);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async loadExistingData(): Promise<void> {
|
|
57
|
+
console.log('š Loading existing data to prevent duplicates...');
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// Load existing emails
|
|
61
|
+
const peopleResponse = await this.client.get('/api/people');
|
|
62
|
+
if (peopleResponse.data && Array.isArray(peopleResponse.data)) {
|
|
63
|
+
peopleResponse.data.forEach((person: any) => {
|
|
64
|
+
if (person.email) {
|
|
65
|
+
this.existingEmails.add(person.email.toLowerCase());
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Load existing companies
|
|
71
|
+
const companiesResponse = await this.client.get('/api/companies');
|
|
72
|
+
if (companiesResponse.data && Array.isArray(companiesResponse.data)) {
|
|
73
|
+
companiesResponse.data.forEach((company: any) => {
|
|
74
|
+
if (company.name) {
|
|
75
|
+
this.existingCompanies.add(company.name.toLowerCase());
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(`ā
Loaded ${this.existingEmails.size} existing emails and ${this.existingCompanies.size} existing companies`);
|
|
81
|
+
} catch (error: any) {
|
|
82
|
+
console.warn('ā ļø Could not load existing data, proceeding without duplicate checking:', error.message);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async createRecords(objectType: string, records: any[]): Promise<BatchResult> {
|
|
87
|
+
try {
|
|
88
|
+
// Filter out duplicates
|
|
89
|
+
const filteredRecords = await this.filterDuplicates(objectType, records);
|
|
90
|
+
|
|
91
|
+
if (filteredRecords.length === 0) {
|
|
92
|
+
return { success: true, count: 0, error: 'All records were duplicates' };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Transform records for Twenty CRM API
|
|
96
|
+
const transformedRecords = records.map(record => this.transformRecordForAPI(objectType, record));
|
|
97
|
+
|
|
98
|
+
const response = await this.client.post(`/api/${objectType}`, transformedRecords);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
success: true,
|
|
102
|
+
count: filteredRecords.length
|
|
103
|
+
};
|
|
104
|
+
} catch (error: any) {
|
|
105
|
+
return {
|
|
106
|
+
success: false,
|
|
107
|
+
count: 0,
|
|
108
|
+
error: error.response?.data?.message || error.message
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private async filterDuplicates(objectType: string, records: any[]): Promise<any[]> {
|
|
114
|
+
return records.filter(record => {
|
|
115
|
+
switch (objectType) {
|
|
116
|
+
case 'people':
|
|
117
|
+
return record.email && !this.existingEmails.has(record.email.toLowerCase());
|
|
118
|
+
case 'companies':
|
|
119
|
+
return record.name && !this.existingCompanies.has(record.name.toLowerCase());
|
|
120
|
+
default:
|
|
121
|
+
return true; // No duplicate checking for other objects
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private transformRecordForAPI(objectType: string, record: any): any {
|
|
127
|
+
switch (objectType) {
|
|
128
|
+
case 'people':
|
|
129
|
+
return {
|
|
130
|
+
name: record.name,
|
|
131
|
+
email: record.email,
|
|
132
|
+
phone: record.phone,
|
|
133
|
+
jobTitle: record.jobTitle,
|
|
134
|
+
company: record.company,
|
|
135
|
+
city: record.city,
|
|
136
|
+
country: record.country,
|
|
137
|
+
source: record.source,
|
|
138
|
+
closeId: record.closeId
|
|
139
|
+
};
|
|
140
|
+
case 'companies':
|
|
141
|
+
return {
|
|
142
|
+
name: record.name,
|
|
143
|
+
domainName: record.domainName,
|
|
144
|
+
industry: record.industry,
|
|
145
|
+
description: record.description,
|
|
146
|
+
phone: record.phone,
|
|
147
|
+
website: record.website,
|
|
148
|
+
source: record.source,
|
|
149
|
+
closeId: record.closeId
|
|
150
|
+
};
|
|
151
|
+
case 'opportunities':
|
|
152
|
+
return {
|
|
153
|
+
name: record.name,
|
|
154
|
+
amount: record.amount,
|
|
155
|
+
closeDate: record.closeDate,
|
|
156
|
+
pipeline: record.pipeline,
|
|
157
|
+
stage: record.stage,
|
|
158
|
+
source: record.source,
|
|
159
|
+
closeId: record.closeId,
|
|
160
|
+
personId: record.personId,
|
|
161
|
+
companyId: record.companyId
|
|
162
|
+
};
|
|
163
|
+
case 'notes':
|
|
164
|
+
return {
|
|
165
|
+
body: record.body,
|
|
166
|
+
source: record.source,
|
|
167
|
+
closeId: record.closeId,
|
|
168
|
+
personId: record.personId,
|
|
169
|
+
companyId: record.companyId,
|
|
170
|
+
opportunityId: record.opportunityId
|
|
171
|
+
};
|
|
172
|
+
case 'activities':
|
|
173
|
+
return {
|
|
174
|
+
subject: record.subject,
|
|
175
|
+
note: record.note,
|
|
176
|
+
type: record.type,
|
|
177
|
+
source: record.source,
|
|
178
|
+
closeId: record.closeId,
|
|
179
|
+
personId: record.personId,
|
|
180
|
+
companyId: record.companyId,
|
|
181
|
+
opportunityId: record.opportunityId
|
|
182
|
+
};
|
|
183
|
+
default:
|
|
184
|
+
return record;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function loadToTwenty(
|
|
190
|
+
data: TransformedData,
|
|
191
|
+
twentyUrl: string,
|
|
192
|
+
twentyKey: string,
|
|
193
|
+
batchSize: number,
|
|
194
|
+
progressBar: SingleBar
|
|
195
|
+
): Promise<LoadResult> {
|
|
196
|
+
const client = new TwentyAPIClient(twentyUrl, twentyKey);
|
|
197
|
+
const errorLog: string[] = [];
|
|
198
|
+
const objectResults = {
|
|
199
|
+
people: { success: 0, errors: 0 },
|
|
200
|
+
companies: { success: 0, errors: 0 },
|
|
201
|
+
opportunities: { success: 0, errors: 0 },
|
|
202
|
+
notes: { success: 0, errors: 0 },
|
|
203
|
+
activities: { success: 0, errors: 0 }
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Test connection first
|
|
207
|
+
console.log('š Testing Twenty CRM connection...');
|
|
208
|
+
const isConnected = await client.testConnection();
|
|
209
|
+
if (!isConnected) {
|
|
210
|
+
throw new Error('Failed to connect to Twenty CRM. Check URL and API key.');
|
|
211
|
+
}
|
|
212
|
+
console.log('ā
Connection successful');
|
|
213
|
+
|
|
214
|
+
// Load existing data for duplicate checking
|
|
215
|
+
await client.loadExistingData();
|
|
216
|
+
|
|
217
|
+
console.log(`š¤ Loading data to Twenty CRM...`);
|
|
218
|
+
|
|
219
|
+
// Process each object type
|
|
220
|
+
const objectTypes = ['people', 'companies', 'opportunities', 'notes', 'activities'] as const;
|
|
221
|
+
|
|
222
|
+
for (const objectType of objectTypes) {
|
|
223
|
+
const records = data[objectType];
|
|
224
|
+
if (!records || records.length === 0) {
|
|
225
|
+
console.log(`āļø Skipping ${objectType} - no records`);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
console.log(`\nš¤ Processing ${objectType} (${records.length} records)...`);
|
|
230
|
+
|
|
231
|
+
// Process in batches
|
|
232
|
+
for (let i = 0; i < records.length; i += batchSize) {
|
|
233
|
+
const batch = records.slice(i, i + batchSize);
|
|
234
|
+
const batchNumber = Math.floor(i / batchSize) + 1;
|
|
235
|
+
const totalBatches = Math.ceil(records.length / batchSize);
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
progressBar.update((i / records.length) * 100, {
|
|
239
|
+
batch: `${batchNumber}/${totalBatches}`,
|
|
240
|
+
records: `${i + 1}-${Math.min(i + batchSize, records.length)}`,
|
|
241
|
+
type: objectType
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const batchResult = await client.createRecords(objectType, batch);
|
|
245
|
+
|
|
246
|
+
if (batchResult.success) {
|
|
247
|
+
objectResults[objectType].success += batchResult.count;
|
|
248
|
+
console.log(`ā
Batch ${batchNumber} (${objectType}): ${batchResult.count} records imported`);
|
|
249
|
+
} else {
|
|
250
|
+
objectResults[objectType].errors += batch.length;
|
|
251
|
+
const errorMsg = `Batch ${batchNumber} (${objectType}): ${batchResult.error}`;
|
|
252
|
+
errorLog.push(errorMsg);
|
|
253
|
+
console.error(`ā ${errorMsg}`);
|
|
254
|
+
}
|
|
255
|
+
} catch (error: any) {
|
|
256
|
+
objectResults[objectType].errors += batch.length;
|
|
257
|
+
const errorMsg = `Batch ${batchNumber} (${objectType}): ${error.message}`;
|
|
258
|
+
errorLog.push(errorMsg);
|
|
259
|
+
console.error(`ā ${errorMsg}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Rate limiting - 100 requests per minute = 600ms between requests
|
|
263
|
+
await new Promise(resolve => setTimeout(resolve, 600));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const totalSuccess = Object.values(objectResults).reduce((sum, result) => sum + result.success, 0);
|
|
268
|
+
const totalErrors = Object.values(objectResults).reduce((sum, result) => sum + result.errors, 0);
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
success: totalSuccess,
|
|
272
|
+
errors: totalErrors,
|
|
273
|
+
errorLog,
|
|
274
|
+
objectResults
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function saveMigrationReport(result: LoadResult, filename: string): void {
|
|
279
|
+
const report = {
|
|
280
|
+
timestamp: new Date().toISOString(),
|
|
281
|
+
summary: {
|
|
282
|
+
total: result.success + result.errors,
|
|
283
|
+
success: result.success,
|
|
284
|
+
errors: result.errors,
|
|
285
|
+
successRate: result.success + result.errors > 0 ? ((result.success / (result.success + result.errors)) * 100).toFixed(2) + '%' : '0%'
|
|
286
|
+
},
|
|
287
|
+
objectResults: result.objectResults,
|
|
288
|
+
errorLog: result.errorLog
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
fs.writeFileSync(filename, JSON.stringify(report, null, 2), 'utf8');
|
|
292
|
+
console.log(`š Migration report saved to ${filename}`);
|
|
293
|
+
}
|
package/src/reporter.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { SingleBar, Presets } from 'cli-progress';
|
|
2
|
+
import { LoadResult } from './load';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
|
|
5
|
+
export function showProgress(total: number): SingleBar {
|
|
6
|
+
const progressBar = new SingleBar({
|
|
7
|
+
format: 'š¤ Migration |{bar}| {percentage}% | {value}/{total} | {type} | Batch: {batch}',
|
|
8
|
+
barCompleteChar: '\u2588',
|
|
9
|
+
barIncompleteChar: '\u2591',
|
|
10
|
+
hideCursor: true
|
|
11
|
+
}, Presets.shades_classic);
|
|
12
|
+
|
|
13
|
+
progressBar.start(total, 0, {
|
|
14
|
+
batch: '0/0',
|
|
15
|
+
records: '0-0',
|
|
16
|
+
type: 'initializing'
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return progressBar;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function generateMigrationReport(result: LoadResult, objects: string): Promise<void> {
|
|
23
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
24
|
+
const reportFilename = `migration-report-${timestamp}.json`;
|
|
25
|
+
const errorLogFilename = `migration-errors-${timestamp}.log`;
|
|
26
|
+
|
|
27
|
+
// Generate detailed report
|
|
28
|
+
const report = {
|
|
29
|
+
timestamp: new Date().toISOString(),
|
|
30
|
+
migration: {
|
|
31
|
+
tool: 'twenty-migrate-close',
|
|
32
|
+
version: '1.0.0',
|
|
33
|
+
objects: objects.split(',').map(obj => obj.trim()),
|
|
34
|
+
dryRun: false
|
|
35
|
+
},
|
|
36
|
+
summary: {
|
|
37
|
+
total: result.success + result.errors,
|
|
38
|
+
success: result.success,
|
|
39
|
+
errors: result.errors,
|
|
40
|
+
successRate: result.success + result.errors > 0 ? ((result.success / (result.success + result.errors)) * 100).toFixed(2) + '%' : '0%'
|
|
41
|
+
},
|
|
42
|
+
objectResults: {
|
|
43
|
+
people: {
|
|
44
|
+
attempted: result.objectResults.people.success + result.objectResults.people.errors,
|
|
45
|
+
success: result.objectResults.people.success,
|
|
46
|
+
errors: result.objectResults.people.errors,
|
|
47
|
+
successRate: result.objectResults.people.success + result.objectResults.people.errors > 0 ?
|
|
48
|
+
((result.objectResults.people.success / (result.objectResults.people.success + result.objectResults.people.errors)) * 100).toFixed(2) + '%' : '0%'
|
|
49
|
+
},
|
|
50
|
+
companies: {
|
|
51
|
+
attempted: result.objectResults.companies.success + result.objectResults.companies.errors,
|
|
52
|
+
success: result.objectResults.companies.success,
|
|
53
|
+
errors: result.objectResults.companies.errors,
|
|
54
|
+
successRate: result.objectResults.companies.success + result.objectResults.companies.errors > 0 ?
|
|
55
|
+
((result.objectResults.companies.success / (result.objectResults.companies.success + result.objectResults.companies.errors)) * 100).toFixed(2) + '%' : '0%'
|
|
56
|
+
},
|
|
57
|
+
opportunities: {
|
|
58
|
+
attempted: result.objectResults.opportunities.success + result.objectResults.opportunities.errors,
|
|
59
|
+
success: result.objectResults.opportunities.success,
|
|
60
|
+
errors: result.objectResults.opportunities.errors,
|
|
61
|
+
successRate: result.objectResults.opportunities.success + result.objectResults.opportunities.errors > 0 ?
|
|
62
|
+
((result.objectResults.opportunities.success / (result.objectResults.opportunities.success + result.objectResults.opportunities.errors)) * 100).toFixed(2) + '%' : '0%'
|
|
63
|
+
},
|
|
64
|
+
notes: {
|
|
65
|
+
attempted: result.objectResults.notes.success + result.objectResults.notes.errors,
|
|
66
|
+
success: result.objectResults.notes.success,
|
|
67
|
+
errors: result.objectResults.notes.errors,
|
|
68
|
+
successRate: result.objectResults.notes.success + result.objectResults.notes.errors > 0 ?
|
|
69
|
+
((result.objectResults.notes.success / (result.objectResults.notes.success + result.objectResults.notes.errors)) * 100).toFixed(2) + '%' : '0%'
|
|
70
|
+
},
|
|
71
|
+
activities: {
|
|
72
|
+
attempted: result.objectResults.activities.success + result.objectResults.activities.errors,
|
|
73
|
+
success: result.objectResults.activities.success,
|
|
74
|
+
errors: result.objectResults.activities.errors,
|
|
75
|
+
successRate: result.objectResults.activities.success + result.objectResults.activities.errors > 0 ?
|
|
76
|
+
((result.objectResults.activities.success / (result.objectResults.activities.success + result.objectResults.activities.errors)) * 100).toFixed(2) + '%' : '0%'
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
errors: result.errorLog,
|
|
80
|
+
recommendations: generateRecommendations(result)
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Save JSON report
|
|
84
|
+
fs.writeFileSync(reportFilename, JSON.stringify(report, null, 2), 'utf8');
|
|
85
|
+
console.log(`š Migration report saved to ${reportFilename}`);
|
|
86
|
+
|
|
87
|
+
// Save error log if there are errors
|
|
88
|
+
if (result.errorLog.length > 0) {
|
|
89
|
+
fs.writeFileSync(errorLogFilename, result.errorLog.join('\n'), 'utf8');
|
|
90
|
+
console.log(`š Error log saved to ${errorLogFilename}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Display summary
|
|
94
|
+
console.log('\nš Migration Summary:');
|
|
95
|
+
console.log(`š Total Records: ${report.summary.total}`);
|
|
96
|
+
console.log(`ā
Successful: ${report.summary.success}`);
|
|
97
|
+
console.log(`ā Errors: ${report.summary.errors}`);
|
|
98
|
+
console.log(`š Success Rate: ${report.summary.successRate}`);
|
|
99
|
+
|
|
100
|
+
console.log('\nš Object Breakdown:');
|
|
101
|
+
Object.entries(report.objectResults).forEach(([objectType, results]) => {
|
|
102
|
+
if (results.attempted > 0) {
|
|
103
|
+
console.log(`š¤ ${objectType}: ${results.success}/${results.attempted} (${results.successRate})`);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (result.errorLog.length > 0) {
|
|
108
|
+
console.log(`\nā Errors encountered: ${result.errorLog.length}`);
|
|
109
|
+
console.log(`š See ${errorLogFilename} for details`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Display recommendations
|
|
113
|
+
if (report.recommendations.length > 0) {
|
|
114
|
+
console.log('\nš” Recommendations:');
|
|
115
|
+
report.recommendations.forEach((rec, index) => {
|
|
116
|
+
console.log(`${index + 1}. ${rec}`);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function generateRecommendations(result: LoadResult): string[] {
|
|
122
|
+
const recommendations: string[] = [];
|
|
123
|
+
|
|
124
|
+
// Overall success rate recommendations
|
|
125
|
+
const totalSuccessRate = result.success + result.errors > 0 ?
|
|
126
|
+
(result.success / (result.success + result.errors)) * 100 : 0;
|
|
127
|
+
|
|
128
|
+
if (totalSuccessRate < 90) {
|
|
129
|
+
recommendations.push('Consider reviewing data quality and formatting before migration');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (totalSuccessRate < 75) {
|
|
133
|
+
recommendations.push('Run migration in dry-run mode first to identify potential issues');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Object-specific recommendations
|
|
137
|
+
Object.entries(result.objectResults).forEach(([objectType, results]) => {
|
|
138
|
+
const successRate = results.success + results.errors > 0 ?
|
|
139
|
+
(results.success / (results.success + results.errors)) * 100 : 0;
|
|
140
|
+
|
|
141
|
+
if (successRate < 80) {
|
|
142
|
+
recommendations.push(`Review ${objectType} data mapping - ${results.errors} records failed`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (results.errors > 0 && successRate < 50) {
|
|
146
|
+
recommendations.push(`Consider manual review of ${objectType} data before retrying`);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Error pattern recommendations
|
|
151
|
+
const errorPatterns = analyzeErrorPatterns(result.errorLog);
|
|
152
|
+
if (errorPatterns.duplicateErrors > 0) {
|
|
153
|
+
recommendations.push('Consider running with duplicate detection disabled if duplicates are expected');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (errorPatterns.rateLimitErrors > 0) {
|
|
157
|
+
recommendations.push('Increase rate limit delay in configuration');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (errorPatterns.validationErrors > 0) {
|
|
161
|
+
recommendations.push('Review data validation rules and field requirements');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return recommendations;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function analyzeErrorPatterns(errorLog: string[]): {
|
|
168
|
+
duplicateErrors: number;
|
|
169
|
+
rateLimitErrors: number;
|
|
170
|
+
validationErrors: number;
|
|
171
|
+
otherErrors: number;
|
|
172
|
+
} {
|
|
173
|
+
const patterns = {
|
|
174
|
+
duplicateErrors: 0,
|
|
175
|
+
rateLimitErrors: 0,
|
|
176
|
+
validationErrors: 0,
|
|
177
|
+
otherErrors: 0
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
errorLog.forEach(error => {
|
|
181
|
+
const errorLower = error.toLowerCase();
|
|
182
|
+
|
|
183
|
+
if (errorLower.includes('duplicate') || errorLower.includes('already exists')) {
|
|
184
|
+
patterns.duplicateErrors++;
|
|
185
|
+
} else if (errorLower.includes('rate limit') || errorLower.includes('too many requests')) {
|
|
186
|
+
patterns.rateLimitErrors++;
|
|
187
|
+
} else if (errorLower.includes('validation') || errorLower.includes('required') || errorLower.includes('invalid')) {
|
|
188
|
+
patterns.validationErrors++;
|
|
189
|
+
} else {
|
|
190
|
+
patterns.otherErrors++;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return patterns;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function saveErrorLog(errorLog: string[], filename: string): void {
|
|
198
|
+
if (errorLog.length === 0) return;
|
|
199
|
+
|
|
200
|
+
const logContent = errorLog.join('\n');
|
|
201
|
+
fs.writeFileSync(filename, logContent, 'utf8');
|
|
202
|
+
console.log(`š Error log saved to ${filename}`);
|
|
203
|
+
}
|