daeda-mcp 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/LICENSE +21 -0
- package/README.md +250 -0
- package/dist/db/config.d.ts +12 -0
- package/dist/db/config.js +53 -0
- package/dist/db/keychain.d.ts +2 -0
- package/dist/db/keychain.js +11 -0
- package/dist/db/schema.d.ts +10 -0
- package/dist/db/schema.js +65 -0
- package/dist/db/sqlite.d.ts +43 -0
- package/dist/db/sqlite.js +280 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +169 -0
- package/dist/sync/csv-loader.d.ts +15 -0
- package/dist/sync/csv-loader.js +450 -0
- package/dist/sync/csv-worker.d.ts +21 -0
- package/dist/sync/csv-worker.js +60 -0
- package/dist/sync/export-api.d.ts +57 -0
- package/dist/sync/export-api.js +355 -0
- package/dist/sync/init-manager.d.ts +5 -0
- package/dist/sync/init-manager.js +274 -0
- package/dist/sync/init-state.d.ts +31 -0
- package/dist/sync/init-state.js +64 -0
- package/dist/sync/seeder.d.ts +1 -0
- package/dist/sync/seeder.js +176 -0
- package/dist/tools/auth.d.ts +26 -0
- package/dist/tools/auth.js +127 -0
- package/dist/tools/query.d.ts +6 -0
- package/dist/tools/query.js +109 -0
- package/package.json +34 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { clearTable, setMetadata, batchInsertContacts, batchInsertCompanies, batchInsertDeals, batchInsertAssociations, } from "../db/sqlite.js";
|
|
2
|
+
function findIdColumn(headers) {
|
|
3
|
+
const idCandidates = [
|
|
4
|
+
"Record ID",
|
|
5
|
+
"hs_object_id",
|
|
6
|
+
"id",
|
|
7
|
+
"ID",
|
|
8
|
+
"Record Id",
|
|
9
|
+
"record_id",
|
|
10
|
+
"recordId",
|
|
11
|
+
"Object ID",
|
|
12
|
+
"object_id",
|
|
13
|
+
"Contact ID",
|
|
14
|
+
"Company ID",
|
|
15
|
+
"Deal ID",
|
|
16
|
+
];
|
|
17
|
+
for (const candidate of idCandidates) {
|
|
18
|
+
if (headers.includes(candidate)) {
|
|
19
|
+
return candidate;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const lowerHeaders = headers.map((h) => h.toLowerCase());
|
|
23
|
+
for (const candidate of idCandidates) {
|
|
24
|
+
const idx = lowerHeaders.indexOf(candidate.toLowerCase());
|
|
25
|
+
if (idx !== -1) {
|
|
26
|
+
return headers[idx];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const recordIdCol = headers.find((h) => h.toLowerCase().includes("record") && h.toLowerCase().includes("id"));
|
|
30
|
+
if (recordIdCol)
|
|
31
|
+
return recordIdCol;
|
|
32
|
+
const anyIdCol = headers.find((h) => h.toLowerCase() === "id" || h.toLowerCase().endsWith("_id"));
|
|
33
|
+
if (anyIdCol)
|
|
34
|
+
return anyIdCol;
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
async function loadCsvWithWorker(filePath, tableName, mapRow, insertBatch, onProgress) {
|
|
38
|
+
await clearTable(tableName);
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const workerUrl = new URL("./csv-worker.ts", import.meta.url);
|
|
41
|
+
const worker = new Worker(workerUrl);
|
|
42
|
+
let idColumn = null;
|
|
43
|
+
let totalInserted = 0;
|
|
44
|
+
let pendingInserts = [];
|
|
45
|
+
worker.onmessage = async (event) => {
|
|
46
|
+
const msg = event.data;
|
|
47
|
+
try {
|
|
48
|
+
switch (msg.type) {
|
|
49
|
+
case "headers": {
|
|
50
|
+
idColumn = findIdColumn(msg.headers);
|
|
51
|
+
if (!idColumn) {
|
|
52
|
+
worker.postMessage({ type: "cancel" });
|
|
53
|
+
worker.terminate();
|
|
54
|
+
reject(new Error(`Could not find ID column. Available columns: ${msg.headers.slice(0, 20).join(", ")}${msg.headers.length > 20 ? "..." : ""}`));
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case "batch": {
|
|
59
|
+
if (!idColumn)
|
|
60
|
+
break;
|
|
61
|
+
const mappedRows = [];
|
|
62
|
+
for (const row of msg.rows) {
|
|
63
|
+
const mapped = mapRow(row, idColumn);
|
|
64
|
+
if (mapped) {
|
|
65
|
+
mappedRows.push(mapped);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (mappedRows.length > 0) {
|
|
69
|
+
const insertPromise = insertBatch(mappedRows).then(() => {
|
|
70
|
+
totalInserted += mappedRows.length;
|
|
71
|
+
onProgress?.(totalInserted);
|
|
72
|
+
});
|
|
73
|
+
pendingInserts.push(insertPromise);
|
|
74
|
+
if (pendingInserts.length >= 3) {
|
|
75
|
+
await Promise.all(pendingInserts);
|
|
76
|
+
pendingInserts = [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case "done": {
|
|
82
|
+
await Promise.all(pendingInserts);
|
|
83
|
+
worker.terminate();
|
|
84
|
+
resolve(totalInserted);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case "error": {
|
|
88
|
+
worker.terminate();
|
|
89
|
+
reject(new Error(msg.error));
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
worker.terminate();
|
|
96
|
+
reject(err);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
worker.onerror = (err) => {
|
|
100
|
+
worker.terminate();
|
|
101
|
+
reject(new Error(`Worker error: ${err.message}`));
|
|
102
|
+
};
|
|
103
|
+
worker.postMessage({
|
|
104
|
+
type: "start",
|
|
105
|
+
filePath,
|
|
106
|
+
batchSize: 5000,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function mapContact(row, idColumn) {
|
|
111
|
+
const id = row[idColumn];
|
|
112
|
+
if (!id)
|
|
113
|
+
return null;
|
|
114
|
+
const email = row.email || row.Email || null;
|
|
115
|
+
const properties = {};
|
|
116
|
+
for (const [key, value] of Object.entries(row)) {
|
|
117
|
+
if (key !== idColumn && value) {
|
|
118
|
+
properties[key] = value;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { id, email, properties };
|
|
122
|
+
}
|
|
123
|
+
function mapCompany(row, idColumn) {
|
|
124
|
+
const id = row[idColumn];
|
|
125
|
+
if (!id)
|
|
126
|
+
return null;
|
|
127
|
+
const domain = row.domain || row.Domain || null;
|
|
128
|
+
const properties = {};
|
|
129
|
+
for (const [key, value] of Object.entries(row)) {
|
|
130
|
+
if (key !== idColumn && value) {
|
|
131
|
+
properties[key] = value;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { id, domain, properties };
|
|
135
|
+
}
|
|
136
|
+
function mapDeal(row, idColumn) {
|
|
137
|
+
const id = row[idColumn];
|
|
138
|
+
if (!id)
|
|
139
|
+
return null;
|
|
140
|
+
const dealname = row.dealname || row["Deal Name"] || row.Dealname || null;
|
|
141
|
+
const properties = {};
|
|
142
|
+
for (const [key, value] of Object.entries(row)) {
|
|
143
|
+
if (key !== idColumn && value) {
|
|
144
|
+
properties[key] = value;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { id, dealname, properties };
|
|
148
|
+
}
|
|
149
|
+
export async function loadContactsCsvFromFile(filePath, onProgress) {
|
|
150
|
+
return loadCsvWithWorker(filePath, "contacts", mapContact, batchInsertContacts, onProgress);
|
|
151
|
+
}
|
|
152
|
+
export async function loadCompaniesCsvFromFile(filePath, onProgress) {
|
|
153
|
+
return loadCsvWithWorker(filePath, "companies", mapCompany, batchInsertCompanies, onProgress);
|
|
154
|
+
}
|
|
155
|
+
export async function loadDealsCsvFromFile(filePath, onProgress) {
|
|
156
|
+
return loadCsvWithWorker(filePath, "deals", mapDeal, batchInsertDeals, onProgress);
|
|
157
|
+
}
|
|
158
|
+
export async function loadAssociationsCsvFromFile(filePath, associationType, onProgress) {
|
|
159
|
+
await clearTable(associationType);
|
|
160
|
+
return new Promise((resolve, reject) => {
|
|
161
|
+
const workerUrl = new URL("./csv-worker.ts", import.meta.url);
|
|
162
|
+
const worker = new Worker(workerUrl);
|
|
163
|
+
let idColumn = null;
|
|
164
|
+
let associatedIdColumn = null;
|
|
165
|
+
let totalInserted = 0;
|
|
166
|
+
let pendingInserts = [];
|
|
167
|
+
worker.onmessage = async (event) => {
|
|
168
|
+
const msg = event.data;
|
|
169
|
+
try {
|
|
170
|
+
switch (msg.type) {
|
|
171
|
+
case "headers": {
|
|
172
|
+
idColumn = findIdColumn(msg.headers);
|
|
173
|
+
if (!idColumn) {
|
|
174
|
+
worker.postMessage({ type: "cancel" });
|
|
175
|
+
worker.terminate();
|
|
176
|
+
reject(new Error(`Could not find ID column in ${associationType} association CSV`));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
associatedIdColumn =
|
|
180
|
+
msg.headers.find((h) => h.toLowerCase().includes("associated") &&
|
|
181
|
+
(h.toLowerCase().includes("id") || h.toLowerCase().includes("record"))) || null;
|
|
182
|
+
if (!associatedIdColumn) {
|
|
183
|
+
worker.postMessage({ type: "cancel" });
|
|
184
|
+
worker.terminate();
|
|
185
|
+
reject(new Error(`Could not find associated IDs column in ${associationType} CSV`));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
case "batch": {
|
|
191
|
+
if (!idColumn || !associatedIdColumn)
|
|
192
|
+
break;
|
|
193
|
+
const associationRows = [];
|
|
194
|
+
for (const row of msg.rows) {
|
|
195
|
+
const fromId = row[idColumn];
|
|
196
|
+
if (!fromId)
|
|
197
|
+
continue;
|
|
198
|
+
const associatedIdsRaw = row[associatedIdColumn];
|
|
199
|
+
if (!associatedIdsRaw)
|
|
200
|
+
continue;
|
|
201
|
+
const associatedIds = associatedIdsRaw
|
|
202
|
+
.split(/[;,]/)
|
|
203
|
+
.map((id) => id.trim())
|
|
204
|
+
.filter(Boolean);
|
|
205
|
+
for (const toId of associatedIds) {
|
|
206
|
+
associationRows.push({ fromId, toId });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (associationRows.length > 0) {
|
|
210
|
+
const insertPromise = batchInsertAssociations(associationType, associationRows).then(() => {
|
|
211
|
+
totalInserted += associationRows.length;
|
|
212
|
+
onProgress?.(totalInserted);
|
|
213
|
+
});
|
|
214
|
+
pendingInserts.push(insertPromise);
|
|
215
|
+
if (pendingInserts.length >= 3) {
|
|
216
|
+
await Promise.all(pendingInserts);
|
|
217
|
+
pendingInserts = [];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case "done": {
|
|
223
|
+
await Promise.all(pendingInserts);
|
|
224
|
+
worker.terminate();
|
|
225
|
+
resolve(totalInserted);
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
case "error": {
|
|
229
|
+
worker.terminate();
|
|
230
|
+
reject(new Error(msg.error));
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
worker.terminate();
|
|
237
|
+
reject(err);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
worker.onerror = (err) => {
|
|
241
|
+
worker.terminate();
|
|
242
|
+
reject(new Error(`Worker error: ${err.message}`));
|
|
243
|
+
};
|
|
244
|
+
worker.postMessage({
|
|
245
|
+
type: "start",
|
|
246
|
+
filePath,
|
|
247
|
+
batchSize: 5000,
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
// Legacy functions for backwards compatibility (loads from string, not recommended for large files)
|
|
252
|
+
function parseCSV(csvData) {
|
|
253
|
+
const lines = csvData.split("\n");
|
|
254
|
+
if (lines.length < 2)
|
|
255
|
+
return [];
|
|
256
|
+
const headers = parseCSVLine(lines[0]);
|
|
257
|
+
const rows = [];
|
|
258
|
+
for (let i = 1; i < lines.length; i++) {
|
|
259
|
+
const line = lines[i].trim();
|
|
260
|
+
if (!line)
|
|
261
|
+
continue;
|
|
262
|
+
const values = parseCSVLine(line);
|
|
263
|
+
const row = {};
|
|
264
|
+
headers.forEach((header, idx) => {
|
|
265
|
+
row[header] = values[idx] || "";
|
|
266
|
+
});
|
|
267
|
+
rows.push(row);
|
|
268
|
+
}
|
|
269
|
+
return rows;
|
|
270
|
+
}
|
|
271
|
+
function parseCSVLine(line) {
|
|
272
|
+
const result = [];
|
|
273
|
+
let current = "";
|
|
274
|
+
let inQuotes = false;
|
|
275
|
+
for (let i = 0; i < line.length; i++) {
|
|
276
|
+
const char = line[i];
|
|
277
|
+
if (char === '"') {
|
|
278
|
+
if (inQuotes && line[i + 1] === '"') {
|
|
279
|
+
current += '"';
|
|
280
|
+
i++;
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
inQuotes = !inQuotes;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else if (char === "," && !inQuotes) {
|
|
287
|
+
result.push(current);
|
|
288
|
+
current = "";
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
current += char;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
result.push(current);
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
export async function loadContactsCsv(csvData, onProgress) {
|
|
298
|
+
await clearTable("contacts");
|
|
299
|
+
const rows = parseCSV(csvData);
|
|
300
|
+
if (rows.length === 0)
|
|
301
|
+
return 0;
|
|
302
|
+
const headers = Object.keys(rows[0]);
|
|
303
|
+
const idColumn = findIdColumn(headers);
|
|
304
|
+
if (!idColumn) {
|
|
305
|
+
throw new Error(`Could not find ID column in contacts CSV. Available columns: ${headers.slice(0, 20).join(", ")}${headers.length > 20 ? "..." : ""}`);
|
|
306
|
+
}
|
|
307
|
+
const contactRows = [];
|
|
308
|
+
for (const row of rows) {
|
|
309
|
+
const id = row[idColumn];
|
|
310
|
+
if (!id)
|
|
311
|
+
continue;
|
|
312
|
+
const email = row.email || row.Email || null;
|
|
313
|
+
const properties = {};
|
|
314
|
+
for (const [key, value] of Object.entries(row)) {
|
|
315
|
+
if (key !== idColumn && value) {
|
|
316
|
+
properties[key] = value;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
contactRows.push({ id, email, properties });
|
|
320
|
+
}
|
|
321
|
+
onProgress?.(0);
|
|
322
|
+
await batchInsertContacts(contactRows);
|
|
323
|
+
onProgress?.(contactRows.length);
|
|
324
|
+
return contactRows.length;
|
|
325
|
+
}
|
|
326
|
+
export async function loadCompaniesCsv(csvData, onProgress) {
|
|
327
|
+
await clearTable("companies");
|
|
328
|
+
const rows = parseCSV(csvData);
|
|
329
|
+
if (rows.length === 0)
|
|
330
|
+
return 0;
|
|
331
|
+
const headers = Object.keys(rows[0]);
|
|
332
|
+
const idColumn = findIdColumn(headers);
|
|
333
|
+
if (!idColumn) {
|
|
334
|
+
throw new Error(`Could not find ID column in companies CSV. Available columns: ${headers.slice(0, 20).join(", ")}${headers.length > 20 ? "..." : ""}`);
|
|
335
|
+
}
|
|
336
|
+
const companyRows = [];
|
|
337
|
+
for (const row of rows) {
|
|
338
|
+
const id = row[idColumn];
|
|
339
|
+
if (!id)
|
|
340
|
+
continue;
|
|
341
|
+
const domain = row.domain || row.Domain || null;
|
|
342
|
+
const properties = {};
|
|
343
|
+
for (const [key, value] of Object.entries(row)) {
|
|
344
|
+
if (key !== idColumn && value) {
|
|
345
|
+
properties[key] = value;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
companyRows.push({ id, domain, properties });
|
|
349
|
+
}
|
|
350
|
+
onProgress?.(0);
|
|
351
|
+
await batchInsertCompanies(companyRows);
|
|
352
|
+
onProgress?.(companyRows.length);
|
|
353
|
+
return companyRows.length;
|
|
354
|
+
}
|
|
355
|
+
export async function loadDealsCsv(csvData, onProgress) {
|
|
356
|
+
await clearTable("deals");
|
|
357
|
+
const rows = parseCSV(csvData);
|
|
358
|
+
if (rows.length === 0)
|
|
359
|
+
return 0;
|
|
360
|
+
const headers = Object.keys(rows[0]);
|
|
361
|
+
const idColumn = findIdColumn(headers);
|
|
362
|
+
if (!idColumn) {
|
|
363
|
+
throw new Error(`Could not find ID column in deals CSV. Available columns: ${headers.slice(0, 20).join(", ")}${headers.length > 20 ? "..." : ""}`);
|
|
364
|
+
}
|
|
365
|
+
const dealRows = [];
|
|
366
|
+
for (const row of rows) {
|
|
367
|
+
const id = row[idColumn];
|
|
368
|
+
if (!id)
|
|
369
|
+
continue;
|
|
370
|
+
const dealname = row.dealname || row["Deal Name"] || row.Dealname || null;
|
|
371
|
+
const properties = {};
|
|
372
|
+
for (const [key, value] of Object.entries(row)) {
|
|
373
|
+
if (key !== idColumn && value) {
|
|
374
|
+
properties[key] = value;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
dealRows.push({ id, dealname, properties });
|
|
378
|
+
}
|
|
379
|
+
onProgress?.(0);
|
|
380
|
+
await batchInsertDeals(dealRows);
|
|
381
|
+
onProgress?.(dealRows.length);
|
|
382
|
+
return dealRows.length;
|
|
383
|
+
}
|
|
384
|
+
export async function loadAssociationsCsv(csvData, associationType, onProgress) {
|
|
385
|
+
await clearTable(associationType);
|
|
386
|
+
const rows = parseCSV(csvData);
|
|
387
|
+
if (rows.length === 0)
|
|
388
|
+
return 0;
|
|
389
|
+
const headers = Object.keys(rows[0]);
|
|
390
|
+
const idColumn = findIdColumn(headers);
|
|
391
|
+
if (!idColumn) {
|
|
392
|
+
throw new Error(`Could not find ID column in ${associationType} association CSV`);
|
|
393
|
+
}
|
|
394
|
+
const associatedIdColumn = headers.find((h) => h.toLowerCase().includes("associated") &&
|
|
395
|
+
(h.toLowerCase().includes("id") || h.toLowerCase().includes("record")));
|
|
396
|
+
if (!associatedIdColumn) {
|
|
397
|
+
throw new Error(`Could not find associated IDs column in ${associationType} CSV`);
|
|
398
|
+
}
|
|
399
|
+
const associationRows = [];
|
|
400
|
+
for (const row of rows) {
|
|
401
|
+
const fromId = row[idColumn];
|
|
402
|
+
if (!fromId)
|
|
403
|
+
continue;
|
|
404
|
+
const associatedIdsRaw = row[associatedIdColumn];
|
|
405
|
+
if (!associatedIdsRaw)
|
|
406
|
+
continue;
|
|
407
|
+
const associatedIds = associatedIdsRaw
|
|
408
|
+
.split(/[;,]/)
|
|
409
|
+
.map((id) => id.trim())
|
|
410
|
+
.filter(Boolean);
|
|
411
|
+
for (const toId of associatedIds) {
|
|
412
|
+
associationRows.push({ fromId, toId });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
onProgress?.(0);
|
|
416
|
+
await batchInsertAssociations(associationType, associationRows);
|
|
417
|
+
onProgress?.(associationRows.length);
|
|
418
|
+
return associationRows.length;
|
|
419
|
+
}
|
|
420
|
+
export async function loadAllData(objects, associations, onProgress) {
|
|
421
|
+
const now = new Date().toISOString();
|
|
422
|
+
onProgress?.("Loading contacts...");
|
|
423
|
+
const contactsCount = await loadContactsCsv(objects.contacts, (n) => onProgress?.(`Contacts: ${n} loaded`));
|
|
424
|
+
await setMetadata("contacts_count", String(contactsCount));
|
|
425
|
+
onProgress?.("Loading companies...");
|
|
426
|
+
const companiesCount = await loadCompaniesCsv(objects.companies, (n) => onProgress?.(`Companies: ${n} loaded`));
|
|
427
|
+
await setMetadata("companies_count", String(companiesCount));
|
|
428
|
+
onProgress?.("Loading deals...");
|
|
429
|
+
const dealsCount = await loadDealsCsv(objects.deals, (n) => onProgress?.(`Deals: ${n} loaded`));
|
|
430
|
+
await setMetadata("deals_count", String(dealsCount));
|
|
431
|
+
const associationCounts = {
|
|
432
|
+
contact_company: 0,
|
|
433
|
+
deal_contact: 0,
|
|
434
|
+
deal_company: 0,
|
|
435
|
+
};
|
|
436
|
+
for (const [assocType, csvData] of Object.entries(associations)) {
|
|
437
|
+
const typedAssocType = assocType;
|
|
438
|
+
onProgress?.(`Loading ${assocType} associations...`);
|
|
439
|
+
associationCounts[typedAssocType] = await loadAssociationsCsv(csvData, typedAssocType, (n) => onProgress?.(`${assocType}: ${n} loaded`));
|
|
440
|
+
await setMetadata(`${assocType}_count`, String(associationCounts[typedAssocType]));
|
|
441
|
+
}
|
|
442
|
+
await setMetadata("last_synced", now);
|
|
443
|
+
await setMetadata("initialized_at", now);
|
|
444
|
+
return {
|
|
445
|
+
contacts: contactsCount,
|
|
446
|
+
companies: companiesCount,
|
|
447
|
+
deals: dealsCount,
|
|
448
|
+
associations: associationCounts,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type CsvWorkerMessage = {
|
|
2
|
+
type: "start";
|
|
3
|
+
filePath: string;
|
|
4
|
+
batchSize?: number;
|
|
5
|
+
} | {
|
|
6
|
+
type: "cancel";
|
|
7
|
+
};
|
|
8
|
+
export type CsvWorkerResponse = {
|
|
9
|
+
type: "batch";
|
|
10
|
+
rows: Record<string, string>[];
|
|
11
|
+
processedCount: number;
|
|
12
|
+
} | {
|
|
13
|
+
type: "headers";
|
|
14
|
+
headers: string[];
|
|
15
|
+
} | {
|
|
16
|
+
type: "done";
|
|
17
|
+
totalCount: number;
|
|
18
|
+
} | {
|
|
19
|
+
type: "error";
|
|
20
|
+
error: string;
|
|
21
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { parse } from "csv-parse";
|
|
3
|
+
let cancelled = false;
|
|
4
|
+
self.onmessage = async (event) => {
|
|
5
|
+
const msg = event.data;
|
|
6
|
+
if (msg.type === "cancel") {
|
|
7
|
+
cancelled = true;
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (msg.type === "start") {
|
|
11
|
+
cancelled = false;
|
|
12
|
+
await processFile(msg.filePath, msg.batchSize ?? 5000);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
async function processFile(filePath, batchSize) {
|
|
16
|
+
try {
|
|
17
|
+
const parser = createReadStream(filePath).pipe(parse({
|
|
18
|
+
columns: true,
|
|
19
|
+
skip_empty_lines: true,
|
|
20
|
+
relax_column_count: true,
|
|
21
|
+
trim: true,
|
|
22
|
+
}));
|
|
23
|
+
let batch = [];
|
|
24
|
+
let totalProcessed = 0;
|
|
25
|
+
let headersSent = false;
|
|
26
|
+
for await (const record of parser) {
|
|
27
|
+
if (cancelled) {
|
|
28
|
+
self.postMessage({ type: "done", totalCount: totalProcessed });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!headersSent) {
|
|
32
|
+
self.postMessage({ type: "headers", headers: Object.keys(record) });
|
|
33
|
+
headersSent = true;
|
|
34
|
+
}
|
|
35
|
+
batch.push(record);
|
|
36
|
+
if (batch.length >= batchSize) {
|
|
37
|
+
totalProcessed += batch.length;
|
|
38
|
+
self.postMessage({
|
|
39
|
+
type: "batch",
|
|
40
|
+
rows: batch,
|
|
41
|
+
processedCount: totalProcessed,
|
|
42
|
+
});
|
|
43
|
+
batch = [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (batch.length > 0) {
|
|
47
|
+
totalProcessed += batch.length;
|
|
48
|
+
self.postMessage({
|
|
49
|
+
type: "batch",
|
|
50
|
+
rows: batch,
|
|
51
|
+
processedCount: totalProcessed,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
self.postMessage({ type: "done", totalCount: totalProcessed });
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
58
|
+
self.postMessage({ type: "error", error: errorMessage });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { AssociationType } from "../db/schema.js";
|
|
2
|
+
export declare function getCachedCsv(name: string): string | null;
|
|
3
|
+
export declare function saveCachedCsv(name: string, data: string): void;
|
|
4
|
+
export declare function hasCachedExports(): boolean;
|
|
5
|
+
export type ExportObjectType = "contacts" | "companies" | "deals";
|
|
6
|
+
export interface ExportStatus {
|
|
7
|
+
id: string;
|
|
8
|
+
status: "PENDING" | "PROCESSING" | "COMPLETE" | "FAILED";
|
|
9
|
+
result?: string;
|
|
10
|
+
message?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface AssociationExportConfig {
|
|
13
|
+
fromObjectType: string;
|
|
14
|
+
toObjectType: string;
|
|
15
|
+
associationType: AssociationType;
|
|
16
|
+
}
|
|
17
|
+
declare const ASSOCIATION_CONFIGS: AssociationExportConfig[];
|
|
18
|
+
export declare function fetchObjectProperties(token: string, objectType: ExportObjectType): Promise<string[]>;
|
|
19
|
+
export declare function startObjectExport(token: string, objectType: ExportObjectType, properties: string[]): Promise<string>;
|
|
20
|
+
export declare function startAssociationExport(token: string, fromObjectType: string, toObjectType: string): Promise<string>;
|
|
21
|
+
export declare function getExportStatus(token: string, exportId: string): Promise<ExportStatus>;
|
|
22
|
+
export interface ExportTask {
|
|
23
|
+
id: string;
|
|
24
|
+
status: "PENDING" | "PROCESSING" | "COMPLETE" | "FAILED" | "CANCELED";
|
|
25
|
+
objectType: string;
|
|
26
|
+
exportName: string;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function listExportTasks(token: string): Promise<ExportTask[]>;
|
|
30
|
+
export interface ReusableExport {
|
|
31
|
+
exportName: string;
|
|
32
|
+
exportId: string;
|
|
33
|
+
createdAt: Date;
|
|
34
|
+
}
|
|
35
|
+
export declare function findReusableExports(token: string, maxAgeDays?: number): Promise<Map<string, ReusableExport>>;
|
|
36
|
+
export declare function pollExportUntilComplete(token: string, exportId: string, onProgress?: (status: string) => void, maxWaitMs?: number, // 5 minutes
|
|
37
|
+
pollIntervalMs?: number): Promise<string>;
|
|
38
|
+
export declare function downloadExportCsv(token: string, downloadUrl: string): Promise<string>;
|
|
39
|
+
export declare function downloadExportCsvToFile(token: string, downloadUrl: string, exportName: string): Promise<string>;
|
|
40
|
+
export interface ExportJob {
|
|
41
|
+
type: "object" | "association";
|
|
42
|
+
objectType?: ExportObjectType;
|
|
43
|
+
associationConfig?: AssociationExportConfig;
|
|
44
|
+
exportId?: string;
|
|
45
|
+
status: "pending" | "exporting" | "downloading" | "complete" | "failed";
|
|
46
|
+
csvData?: string;
|
|
47
|
+
error?: string;
|
|
48
|
+
}
|
|
49
|
+
export declare function runAllExports(token: string, onProgress?: (message: string) => void, useCache?: boolean): Promise<{
|
|
50
|
+
objects: Record<ExportObjectType, string>;
|
|
51
|
+
associations: Record<AssociationType, string>;
|
|
52
|
+
}>;
|
|
53
|
+
export declare function validateToken(token: string): Promise<{
|
|
54
|
+
valid: boolean;
|
|
55
|
+
error?: string;
|
|
56
|
+
}>;
|
|
57
|
+
export { ASSOCIATION_CONFIGS };
|