@whaly/connector-sdk 0.2.0 → 0.3.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/README.md +92 -0
- package/dist/index.d.mts +458 -2
- package/dist/index.d.ts +458 -2
- package/dist/index.js +1181 -148
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1147 -147
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -5
package/dist/index.js
CHANGED
|
@@ -35,9 +35,16 @@ __export(index_exports, {
|
|
|
35
35
|
Authenticator: () => Authenticator,
|
|
36
36
|
BATCH_INTERVAL_MS: () => BATCH_INTERVAL_MS,
|
|
37
37
|
BigQueryTarget: () => BigQueryTarget,
|
|
38
|
+
CloudStorageService: () => CloudStorageService,
|
|
38
39
|
CounterMetric: () => CounterMetric,
|
|
40
|
+
CsvExtractionConfigBuilder: () => CsvExtractionConfigBuilder,
|
|
39
41
|
DEFAULT_MAX_CONCURRENT_STREAMS: () => DEFAULT_MAX_CONCURRENT_STREAMS,
|
|
40
42
|
EXECUTION_TIME_METRIC_NAME: () => EXECUTION_TIME_METRIC_NAME,
|
|
43
|
+
ExcelExtractionConfigBuilder: () => ExcelExtractionConfigBuilder,
|
|
44
|
+
FileFormat: () => FileFormat,
|
|
45
|
+
FilePatterns: () => FilePatterns,
|
|
46
|
+
FileStream: () => FileStream,
|
|
47
|
+
FileTap: () => FileTap,
|
|
41
48
|
GCSStateProvider: () => GCSStateProvider,
|
|
42
49
|
ITarget: () => ITarget,
|
|
43
50
|
MissingFieldInSchemaError: () => MissingFieldInSchemaError,
|
|
@@ -50,16 +57,33 @@ __export(index_exports, {
|
|
|
50
57
|
ReplicationMethodMessage: () => ReplicationMethodMessage,
|
|
51
58
|
SchemaMessage: () => SchemaMessage,
|
|
52
59
|
SchemaValidationError: () => SchemaValidationError,
|
|
60
|
+
SftpClient: () => import_ssh2_sftp_client2.default,
|
|
61
|
+
SftpService: () => SftpService,
|
|
53
62
|
StateMessage: () => StateMessage,
|
|
54
63
|
StateService: () => StateService,
|
|
55
64
|
Stream: () => Stream,
|
|
56
65
|
StreamWarehouseSyncService: () => StreamWarehouseSyncService,
|
|
57
66
|
Tap: () => Tap,
|
|
67
|
+
VariableExtractors: () => VariableExtractors,
|
|
58
68
|
_greaterThan: () => _greaterThan,
|
|
59
69
|
addWhalyFields: () => addWhalyFields,
|
|
70
|
+
checkCsvHeaderRow: () => checkCsvHeaderRow,
|
|
71
|
+
createCellReference: () => createCellReference,
|
|
72
|
+
createCsvStreamConfig: () => createCsvStreamConfig,
|
|
73
|
+
createExcelGenerator: () => createExcelGenerator,
|
|
74
|
+
createExcelStreamConfig: () => createExcelStreamConfig,
|
|
60
75
|
createTemporaryFileStream: () => createTemporaryFileStream,
|
|
76
|
+
csvFieldsToJsonSchema: () => csvFieldsToJsonSchema,
|
|
77
|
+
excelColumnToIndex: () => excelColumnToIndex,
|
|
78
|
+
excelFieldsToJsonSchema: () => excelFieldsToJsonSchema,
|
|
79
|
+
excelRowToIndex: () => excelRowToIndex,
|
|
80
|
+
extractExcelRows: () => extractExcelRows,
|
|
81
|
+
extractPrimaryKeysFromCsvConfig: () => extractPrimaryKeysFromCsvConfig,
|
|
82
|
+
extractPrimaryKeysFromExcelFields: () => extractPrimaryKeysFromExcelFields,
|
|
61
83
|
extractStateForStream: () => extractStateForStream,
|
|
84
|
+
fieldTypeToJsonSchema: () => fieldTypeToJsonSchema,
|
|
62
85
|
finalizeStateProgressMarkers: () => finalizeStateProgressMarkers,
|
|
86
|
+
findAllMatchingConfigs: () => findAllMatchingConfigs,
|
|
63
87
|
flattenSchema: () => flattenSchema,
|
|
64
88
|
getAllMetrics: () => getAllMetrics,
|
|
65
89
|
getAxiosInstance: () => getAxiosInstance,
|
|
@@ -72,15 +96,24 @@ __export(index_exports, {
|
|
|
72
96
|
gracefulExit: () => gracefulExit,
|
|
73
97
|
haltAndCatchFire: () => haltAndCatchFire,
|
|
74
98
|
incrementStreamState: () => incrementStreamState,
|
|
99
|
+
indexToExcelColumn: () => indexToExcelColumn,
|
|
100
|
+
indexToExcelRow: () => indexToExcelRow,
|
|
75
101
|
loadJson: () => loadJson,
|
|
76
102
|
loadSchema: () => loadSchema,
|
|
77
103
|
logger: () => logger,
|
|
104
|
+
parseCellReference: () => parseCellReference,
|
|
78
105
|
postFormDataApiCall: () => postFormDataApiCall,
|
|
79
106
|
postJSONApiCall: () => postJSONApiCall,
|
|
80
107
|
postUrlEncodedApiCall: () => postUrlEncodedApiCall,
|
|
81
108
|
printMemoryFootprint: () => printMemoryFootprint,
|
|
109
|
+
processFileStreams: () => processFileStreams,
|
|
82
110
|
removeParasiteProperties: () => removeParasiteProperties,
|
|
83
|
-
|
|
111
|
+
rowGeneratorFromCsv: () => rowGeneratorFromCsv,
|
|
112
|
+
safePath: () => safePath,
|
|
113
|
+
unzip: () => unzip,
|
|
114
|
+
validateExcelConfig: () => validateExcelConfig,
|
|
115
|
+
writeDataToCsv: () => writeDataToCsv,
|
|
116
|
+
writeGeneratorToCSV: () => writeGeneratorToCSV
|
|
84
117
|
});
|
|
85
118
|
module.exports = __toCommonJS(index_exports);
|
|
86
119
|
|
|
@@ -466,6 +499,7 @@ var haltAndCatchFire = async (errorType, errorText, errorDebugText) => {
|
|
|
466
499
|
var ReplicationMethod = /* @__PURE__ */ ((ReplicationMethod2) => {
|
|
467
500
|
ReplicationMethod2["INCREMENTAL"] = "INCREMENTAL";
|
|
468
501
|
ReplicationMethod2["FULL_TABLE"] = "FULL_TABLE";
|
|
502
|
+
ReplicationMethod2["APPEND"] = "APPEND";
|
|
469
503
|
return ReplicationMethod2;
|
|
470
504
|
})(ReplicationMethod || {});
|
|
471
505
|
|
|
@@ -1590,6 +1624,8 @@ var StreamWarehouseSyncService = class {
|
|
|
1590
1624
|
mergeQueries = this.getReplaceQueries();
|
|
1591
1625
|
} else if (replicationMethod === "INCREMENTAL") {
|
|
1592
1626
|
mergeQueries = this.getMergeQueries();
|
|
1627
|
+
} else if (replicationMethod === "APPEND") {
|
|
1628
|
+
mergeQueries = this.getAppendQueries();
|
|
1593
1629
|
} else if (replicationMethod === "LOG_BASED") {
|
|
1594
1630
|
mergeQueries = this.getMergeQueries();
|
|
1595
1631
|
}
|
|
@@ -1771,15 +1807,15 @@ var import_fs_extra = __toESM(require("fs-extra"));
|
|
|
1771
1807
|
var import_uniqueId = __toESM(require("lodash/uniqueId.js"));
|
|
1772
1808
|
var createTemporaryFileStream = (streamId) => {
|
|
1773
1809
|
import_fs_extra.default.ensureDirSync("./tmp");
|
|
1774
|
-
const
|
|
1775
|
-
const writeStream = import_fs_extra.default.createWriteStream(
|
|
1810
|
+
const path2 = `./tmp/${safePath(streamId)}-${(0, import_uniqueId.default)()}.tmp`;
|
|
1811
|
+
const writeStream = import_fs_extra.default.createWriteStream(path2, { flags: "w" });
|
|
1776
1812
|
return {
|
|
1777
1813
|
stream: writeStream,
|
|
1778
|
-
path
|
|
1814
|
+
path: path2
|
|
1779
1815
|
};
|
|
1780
1816
|
};
|
|
1781
|
-
function safePath(
|
|
1782
|
-
let formattedPath =
|
|
1817
|
+
function safePath(path2) {
|
|
1818
|
+
let formattedPath = path2;
|
|
1783
1819
|
formattedPath = formattedPath.replace(/^The\s+/gi, "");
|
|
1784
1820
|
formattedPath = formattedPath.replace(/\s+(?:\(|\[)?Dis(?:c|k) (?:One|Two|Three|Four|Five|Six|Seven|Eight|Nine|Ten|\d)+(?:\)|\])?/gi, "");
|
|
1785
1821
|
formattedPath = formattedPath.replace(/\s+/gi, "_");
|
|
@@ -2088,7 +2124,8 @@ var ITarget = class _ITarget {
|
|
|
2088
2124
|
}
|
|
2089
2125
|
});
|
|
2090
2126
|
streamState.setSchema(schema);
|
|
2091
|
-
|
|
2127
|
+
const streamReplicationMethod = this.streams[streamId]?.getReplicationMethod();
|
|
2128
|
+
if (message.keyProperties.length === 0 && streamReplicationMethod !== "APPEND" /* APPEND */) {
|
|
2092
2129
|
throw new Error(`StreamId: ${message.stream} - \`key_properties\` field is required and can't be empty in SCHEMA message.
|
|
2093
2130
|
Received SCHEMA message: ${JSON.stringify(message)}`);
|
|
2094
2131
|
}
|
|
@@ -2152,7 +2189,8 @@ var ITarget = class _ITarget {
|
|
|
2152
2189
|
return false;
|
|
2153
2190
|
}
|
|
2154
2191
|
const batchedRecords = stream.getBatchedRowCount();
|
|
2155
|
-
|
|
2192
|
+
const method = stream.getReplicationMethod();
|
|
2193
|
+
if ((method === "INCREMENTAL" || method === "APPEND") && batchedRecords > 1e6) {
|
|
2156
2194
|
logger.info(`\u{1F9CA} Stream=${streamId} has reached the maximum amount of batched records. We'll trigger a flush of ALL streams in the Warehouse.`);
|
|
2157
2195
|
return true;
|
|
2158
2196
|
} else {
|
|
@@ -2221,6 +2259,1071 @@ var ITarget = class _ITarget {
|
|
|
2221
2259
|
};
|
|
2222
2260
|
};
|
|
2223
2261
|
|
|
2262
|
+
// src/sdk/file-processing/types.ts
|
|
2263
|
+
var FileFormat = /* @__PURE__ */ ((FileFormat2) => {
|
|
2264
|
+
FileFormat2["CSV"] = "CSV";
|
|
2265
|
+
FileFormat2["EXCEL"] = "EXCEL";
|
|
2266
|
+
return FileFormat2;
|
|
2267
|
+
})(FileFormat || {});
|
|
2268
|
+
|
|
2269
|
+
// src/sdk/file-processing/schema-utils.ts
|
|
2270
|
+
function fieldTypeToJsonSchema(fieldType) {
|
|
2271
|
+
switch (fieldType) {
|
|
2272
|
+
case "STRING":
|
|
2273
|
+
return { type: ["null", "string"] };
|
|
2274
|
+
case "FLOAT":
|
|
2275
|
+
return { type: ["null", "number"] };
|
|
2276
|
+
case "TIMESTAMP":
|
|
2277
|
+
return { type: ["null", "string"], format: "date-time" };
|
|
2278
|
+
default:
|
|
2279
|
+
return { type: ["null", "string"] };
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
function excelFieldsToJsonSchema(fields) {
|
|
2283
|
+
const properties = {};
|
|
2284
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
2285
|
+
properties[key] = fieldTypeToJsonSchema(field.type);
|
|
2286
|
+
}
|
|
2287
|
+
return { type: "object", properties };
|
|
2288
|
+
}
|
|
2289
|
+
function csvFieldsToJsonSchema(config) {
|
|
2290
|
+
const properties = {};
|
|
2291
|
+
if (Array.isArray(config)) {
|
|
2292
|
+
for (const field of config) {
|
|
2293
|
+
properties[field.key] = fieldTypeToJsonSchema(field.type);
|
|
2294
|
+
}
|
|
2295
|
+
} else {
|
|
2296
|
+
for (const [, field] of Object.entries(config)) {
|
|
2297
|
+
properties[field.key] = fieldTypeToJsonSchema(field.type);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
return { type: "object", properties };
|
|
2301
|
+
}
|
|
2302
|
+
function extractPrimaryKeysFromCsvConfig(config) {
|
|
2303
|
+
if (Array.isArray(config)) {
|
|
2304
|
+
return config.map((f) => f.key);
|
|
2305
|
+
}
|
|
2306
|
+
return Object.values(config).map((f) => f.key);
|
|
2307
|
+
}
|
|
2308
|
+
function extractPrimaryKeysFromExcelFields(fields) {
|
|
2309
|
+
return Object.entries(fields).filter(([, field]) => {
|
|
2310
|
+
return field.primaryKey === true || field.primaryKey === true;
|
|
2311
|
+
}).map(([key]) => key);
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
// src/sdk/file-processing/excel/reader.ts
|
|
2315
|
+
var import_xlsx = __toESM(require("xlsx"));
|
|
2316
|
+
var import_path = __toESM(require("path"));
|
|
2317
|
+
var excelColumnToIndex = (columnLetter) => {
|
|
2318
|
+
const letters = columnLetter.toUpperCase();
|
|
2319
|
+
let index = 0;
|
|
2320
|
+
for (let i = 0; i < letters.length; i++) {
|
|
2321
|
+
const charCode = letters.charCodeAt(i);
|
|
2322
|
+
if (charCode < 65 || charCode > 90) {
|
|
2323
|
+
throw new Error(`Invalid column letter: ${columnLetter}`);
|
|
2324
|
+
}
|
|
2325
|
+
index = index * 26 + (charCode - 64);
|
|
2326
|
+
}
|
|
2327
|
+
return index - 1;
|
|
2328
|
+
};
|
|
2329
|
+
var indexToExcelColumn = (columnIndex) => {
|
|
2330
|
+
if (columnIndex < 0) {
|
|
2331
|
+
throw new Error(`Invalid column index: ${columnIndex}. Must be >= 0`);
|
|
2332
|
+
}
|
|
2333
|
+
let result = "";
|
|
2334
|
+
let index = columnIndex;
|
|
2335
|
+
while (true) {
|
|
2336
|
+
result = String.fromCharCode(65 + index % 26) + result;
|
|
2337
|
+
index = Math.floor(index / 26);
|
|
2338
|
+
if (index === 0) break;
|
|
2339
|
+
index--;
|
|
2340
|
+
}
|
|
2341
|
+
return result;
|
|
2342
|
+
};
|
|
2343
|
+
var excelRowToIndex = (rowNumber) => {
|
|
2344
|
+
const rowNum = typeof rowNumber === "string" ? parseInt(rowNumber, 10) : rowNumber;
|
|
2345
|
+
if (isNaN(rowNum) || rowNum < 1) {
|
|
2346
|
+
throw new Error(`Invalid row number: ${rowNumber}. Must be >= 1`);
|
|
2347
|
+
}
|
|
2348
|
+
return rowNum - 1;
|
|
2349
|
+
};
|
|
2350
|
+
var indexToExcelRow = (index) => {
|
|
2351
|
+
if (index < 0) {
|
|
2352
|
+
throw new Error(`Invalid index: ${index}. Must be >= 0`);
|
|
2353
|
+
}
|
|
2354
|
+
return index + 1;
|
|
2355
|
+
};
|
|
2356
|
+
var parseCellReference = (cellRef) => {
|
|
2357
|
+
const match = cellRef.match(/^([A-Z]+)(\d+)$/);
|
|
2358
|
+
if (!match) {
|
|
2359
|
+
throw new Error(`Invalid cell reference: ${cellRef}. Expected format like "A1", "Z24", "AA100"`);
|
|
2360
|
+
}
|
|
2361
|
+
const [, columnLetter, rowNumber] = match;
|
|
2362
|
+
return {
|
|
2363
|
+
columnIndex: excelColumnToIndex(columnLetter),
|
|
2364
|
+
rowIndex: excelRowToIndex(rowNumber)
|
|
2365
|
+
};
|
|
2366
|
+
};
|
|
2367
|
+
var createCellReference = (columnIndex, rowIndex) => {
|
|
2368
|
+
return indexToExcelColumn(columnIndex) + indexToExcelRow(rowIndex);
|
|
2369
|
+
};
|
|
2370
|
+
var extractSingleSheetRows = async (fileName, conf) => {
|
|
2371
|
+
console.log("Extracting single sheet rows from file: %s", fileName);
|
|
2372
|
+
const workbook = import_xlsx.default.readFile(fileName, { dense: true });
|
|
2373
|
+
const sheetKeys = Object.keys(workbook.Sheets);
|
|
2374
|
+
const sheet = conf.sheetName ? workbook.Sheets[conf.sheetName] : workbook.Sheets[sheetKeys[0]];
|
|
2375
|
+
if (!sheet) {
|
|
2376
|
+
throw new Error(`Sheet ${conf.sheetName} not found in workbook`);
|
|
2377
|
+
}
|
|
2378
|
+
const sheetData = sheet["!data"];
|
|
2379
|
+
if (!sheetData) {
|
|
2380
|
+
throw new Error(`No data in ${conf.sheetName || "first"} sheet`);
|
|
2381
|
+
}
|
|
2382
|
+
if (sheetData.length === 0) {
|
|
2383
|
+
throw new Error(`Sheet ${conf.sheetName || "first"} is empty`);
|
|
2384
|
+
}
|
|
2385
|
+
const variables = conf.fileNameVariablesExtractor(fileName, workbook);
|
|
2386
|
+
const data = [];
|
|
2387
|
+
let rowIdx = 0;
|
|
2388
|
+
const minColumnIndex = Object.values(conf.columns).reduce((min, column) => {
|
|
2389
|
+
return column.column ? Math.min(min, excelColumnToIndex(column.column)) : min;
|
|
2390
|
+
}, Number.MAX_SAFE_INTEGER);
|
|
2391
|
+
for (const row of sheetData) {
|
|
2392
|
+
if (rowIdx < conf.numberOfRowsToSkip) {
|
|
2393
|
+
rowIdx++;
|
|
2394
|
+
continue;
|
|
2395
|
+
}
|
|
2396
|
+
if (!row) {
|
|
2397
|
+
console.log("Empty row at index: %s, skipped processing.", rowIdx);
|
|
2398
|
+
continue;
|
|
2399
|
+
}
|
|
2400
|
+
if (minColumnIndex !== Number.MAX_SAFE_INTEGER && !row[minColumnIndex]?.v) {
|
|
2401
|
+
console.log("stopping processing because of empty value in the first data column (idx: %s)", minColumnIndex);
|
|
2402
|
+
break;
|
|
2403
|
+
}
|
|
2404
|
+
const rowData = Object.entries(conf.columns).reduce((acc, [key, column]) => {
|
|
2405
|
+
if (column.variableName) {
|
|
2406
|
+
if (!variables) {
|
|
2407
|
+
throw new Error(`No variables extracted from filename, cannot extract derived field ${key}`);
|
|
2408
|
+
}
|
|
2409
|
+
acc[key] = variables[column.variableName] || "";
|
|
2410
|
+
} else {
|
|
2411
|
+
const colIndex = excelColumnToIndex(column.column);
|
|
2412
|
+
acc[key] = row[colIndex]?.v || "";
|
|
2413
|
+
}
|
|
2414
|
+
return acc;
|
|
2415
|
+
}, {});
|
|
2416
|
+
data.push(rowData);
|
|
2417
|
+
rowIdx++;
|
|
2418
|
+
}
|
|
2419
|
+
console.log("Extracted %s rows from sheet %s", data.length, conf.sheetName || "first");
|
|
2420
|
+
return data;
|
|
2421
|
+
};
|
|
2422
|
+
var extractExcelRows = async (localFilePath, conf) => {
|
|
2423
|
+
let data;
|
|
2424
|
+
if (conf.type === "single-sheet-extraction") {
|
|
2425
|
+
data = await extractSingleSheetRows(localFilePath, conf);
|
|
2426
|
+
} else if (conf.type === "processor") {
|
|
2427
|
+
const workbook = import_xlsx.default.readFile(localFilePath, { dense: true });
|
|
2428
|
+
data = await conf.processor(workbook);
|
|
2429
|
+
} else {
|
|
2430
|
+
throw new Error(`Unsupported configuration type: ${conf.type}`);
|
|
2431
|
+
}
|
|
2432
|
+
return data;
|
|
2433
|
+
};
|
|
2434
|
+
var validateExcelConfig = (config) => {
|
|
2435
|
+
if (!config.extension) {
|
|
2436
|
+
throw new Error("Extension is required");
|
|
2437
|
+
}
|
|
2438
|
+
if (!config.tableName) {
|
|
2439
|
+
throw new Error("Table name is required");
|
|
2440
|
+
}
|
|
2441
|
+
if (!config.fileNameValidator || typeof config.fileNameValidator !== "function") {
|
|
2442
|
+
throw new Error("File name validator function is required");
|
|
2443
|
+
}
|
|
2444
|
+
if (!config.fileNameVariablesExtractor || typeof config.fileNameVariablesExtractor !== "function") {
|
|
2445
|
+
throw new Error("File name variables extractor function is required");
|
|
2446
|
+
}
|
|
2447
|
+
if (config.type === "single-sheet-extraction" && (!config.columns || Object.keys(config.columns).length === 0)) {
|
|
2448
|
+
throw new Error("At least one column configuration is required for single-sheet-extraction");
|
|
2449
|
+
}
|
|
2450
|
+
if (config.type === "single-sheet-extraction") {
|
|
2451
|
+
Object.entries(config.columns).forEach(([key, column]) => {
|
|
2452
|
+
if (!column.type) {
|
|
2453
|
+
throw new Error(`Column ${key} must have a type`);
|
|
2454
|
+
}
|
|
2455
|
+
if (column.column) {
|
|
2456
|
+
const colLetter = column.column;
|
|
2457
|
+
if (!/^[A-Z]+$/i.test(colLetter)) {
|
|
2458
|
+
throw new Error(`Invalid column letter format: ${colLetter}`);
|
|
2459
|
+
}
|
|
2460
|
+
} else if (column.variableName) {
|
|
2461
|
+
} else {
|
|
2462
|
+
throw new Error(`Column ${key} must specify either 'column' or 'variableName'`);
|
|
2463
|
+
}
|
|
2464
|
+
});
|
|
2465
|
+
if (config.numberOfRowsToSkip === void 0 || config.numberOfRowsToSkip < 0) {
|
|
2466
|
+
throw new Error("Number of rows to skip must be specified and non-negative");
|
|
2467
|
+
}
|
|
2468
|
+
} else if (config.type === "processor") {
|
|
2469
|
+
if (!config.processor || typeof config.processor !== "function") {
|
|
2470
|
+
throw new Error("Processor function is required for processor type");
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
};
|
|
2474
|
+
var findAllMatchingConfigs = (filename, configs) => {
|
|
2475
|
+
const fileInfo = import_path.default.parse(filename);
|
|
2476
|
+
return configs.filter(
|
|
2477
|
+
(config) => config.fileNameValidator(fileInfo.base) && config.extension === fileInfo.ext.replace(".", "")
|
|
2478
|
+
);
|
|
2479
|
+
};
|
|
2480
|
+
async function* createExcelGenerator(data) {
|
|
2481
|
+
for (const row of data) {
|
|
2482
|
+
yield row;
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
// src/sdk/file-processing/excel/config-builder.ts
|
|
2487
|
+
var ExcelExtractionConfigBuilder = class _ExcelExtractionConfigBuilder {
|
|
2488
|
+
config = {
|
|
2489
|
+
fileNameValidator: () => true,
|
|
2490
|
+
fileNameVariablesExtractor: () => ({}),
|
|
2491
|
+
replicationMethod: "FULL_TABLE" /* FULL_TABLE */
|
|
2492
|
+
};
|
|
2493
|
+
static create() {
|
|
2494
|
+
return new _ExcelExtractionConfigBuilder();
|
|
2495
|
+
}
|
|
2496
|
+
extension(ext) {
|
|
2497
|
+
this.config.extension = ext;
|
|
2498
|
+
return this;
|
|
2499
|
+
}
|
|
2500
|
+
tableName(name) {
|
|
2501
|
+
this.config.tableName = name;
|
|
2502
|
+
return this;
|
|
2503
|
+
}
|
|
2504
|
+
fileValidator(validator) {
|
|
2505
|
+
this.config.fileNameValidator = validator;
|
|
2506
|
+
return this;
|
|
2507
|
+
}
|
|
2508
|
+
variablesExtractor(extractor) {
|
|
2509
|
+
this.config.fileNameVariablesExtractor = extractor;
|
|
2510
|
+
return this;
|
|
2511
|
+
}
|
|
2512
|
+
replicationMethod(method) {
|
|
2513
|
+
this.config.replicationMethod = method;
|
|
2514
|
+
return this;
|
|
2515
|
+
}
|
|
2516
|
+
columns(cols) {
|
|
2517
|
+
if (this.config.type === "single-sheet-extraction") {
|
|
2518
|
+
this.config.columns = cols;
|
|
2519
|
+
} else {
|
|
2520
|
+
throw new Error("Columns can only be set for single-sheet-extraction");
|
|
2521
|
+
}
|
|
2522
|
+
return this;
|
|
2523
|
+
}
|
|
2524
|
+
singleSheet(sheetName, skipRows = 0) {
|
|
2525
|
+
this.config.type = "single-sheet-extraction";
|
|
2526
|
+
if (sheetName !== void 0) {
|
|
2527
|
+
this.config.sheetName = sheetName;
|
|
2528
|
+
}
|
|
2529
|
+
this.config.numberOfRowsToSkip = skipRows;
|
|
2530
|
+
return this;
|
|
2531
|
+
}
|
|
2532
|
+
processor(processorFn) {
|
|
2533
|
+
this.config.type = "processor";
|
|
2534
|
+
this.config.processor = processorFn;
|
|
2535
|
+
return this;
|
|
2536
|
+
}
|
|
2537
|
+
build() {
|
|
2538
|
+
if (!this.config.type) {
|
|
2539
|
+
throw new Error("Configuration type must be specified (singleSheet or processor)");
|
|
2540
|
+
}
|
|
2541
|
+
if (!this.config.extension) {
|
|
2542
|
+
throw new Error("Extension must be set");
|
|
2543
|
+
}
|
|
2544
|
+
if (!this.config.tableName) {
|
|
2545
|
+
throw new Error("Table name must be set");
|
|
2546
|
+
}
|
|
2547
|
+
if (!this.config.replicationMethod) {
|
|
2548
|
+
throw new Error("Replication method must be set");
|
|
2549
|
+
}
|
|
2550
|
+
if (typeof this.config.fileNameValidator !== "function") {
|
|
2551
|
+
throw new Error("fileNameValidator must be a function");
|
|
2552
|
+
}
|
|
2553
|
+
if (typeof this.config.fileNameVariablesExtractor !== "function") {
|
|
2554
|
+
throw new Error("fileNameVariablesExtractor must be a function");
|
|
2555
|
+
}
|
|
2556
|
+
if (this.config.type === "single-sheet-extraction") {
|
|
2557
|
+
const singleSheetConfig = this.config;
|
|
2558
|
+
if (!singleSheetConfig.columns || Object.keys(singleSheetConfig.columns).length === 0) {
|
|
2559
|
+
throw new Error("Columns must be set with at least one column for single-sheet-extraction");
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
if (this.config.type === "processor") {
|
|
2563
|
+
const processorConfig = this.config;
|
|
2564
|
+
if (typeof processorConfig.processor !== "function") {
|
|
2565
|
+
throw new Error("Processor function must be set for processor configs");
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
return this.config;
|
|
2569
|
+
}
|
|
2570
|
+
};
|
|
2571
|
+
|
|
2572
|
+
// src/sdk/file-processing/csv/reader.ts
|
|
2573
|
+
var import_csv_parser = __toESM(require("csv-parser"));
|
|
2574
|
+
var import_fs2 = require("fs");
|
|
2575
|
+
var import_util3 = require("util");
|
|
2576
|
+
var import_stream2 = require("stream");
|
|
2577
|
+
var iconv = __toESM(require("iconv-lite"));
|
|
2578
|
+
var logPrefix = "[csvReader]";
|
|
2579
|
+
var createStripBomTransform = () => {
|
|
2580
|
+
let isFirstChunk = true;
|
|
2581
|
+
return new import_stream2.Transform({
|
|
2582
|
+
transform(chunk2, encoding, callback) {
|
|
2583
|
+
if (isFirstChunk) {
|
|
2584
|
+
isFirstChunk = false;
|
|
2585
|
+
if (chunk2.length >= 3 && chunk2[0] === 239 && chunk2[1] === 187 && chunk2[2] === 191) {
|
|
2586
|
+
chunk2 = chunk2.slice(3);
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
callback(null, chunk2);
|
|
2590
|
+
}
|
|
2591
|
+
});
|
|
2592
|
+
};
|
|
2593
|
+
var getStringHexInfo = (str) => {
|
|
2594
|
+
const bytes = Buffer.from(str, "utf8");
|
|
2595
|
+
const hex = bytes.toString("hex").match(/.{2}/g)?.join(" ") || "";
|
|
2596
|
+
return {
|
|
2597
|
+
original: str,
|
|
2598
|
+
length: str.length,
|
|
2599
|
+
hex,
|
|
2600
|
+
trimmed: str.trim()
|
|
2601
|
+
};
|
|
2602
|
+
};
|
|
2603
|
+
var formatHexComparison = (expected, actual) => {
|
|
2604
|
+
const expectedInfo = getStringHexInfo(expected);
|
|
2605
|
+
const actualInfo = getStringHexInfo(actual);
|
|
2606
|
+
return `
|
|
2607
|
+
Expected: "${expected}" (${expectedInfo.length} chars) [${expectedInfo.hex}]
|
|
2608
|
+
Actual: "${actual}" (${actualInfo.length} chars) [${actualInfo.hex}]`;
|
|
2609
|
+
};
|
|
2610
|
+
async function* rowGeneratorFromCsv(path2, fileConfig) {
|
|
2611
|
+
const isGeneratorConfigArray = Array.isArray(fileConfig.fields);
|
|
2612
|
+
const csvOptions = { separator: fileConfig.separator };
|
|
2613
|
+
if (isGeneratorConfigArray) {
|
|
2614
|
+
csvOptions.headers = false;
|
|
2615
|
+
}
|
|
2616
|
+
const csvStream = (0, import_csv_parser.default)(csvOptions);
|
|
2617
|
+
const syncedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2618
|
+
let initialReadStream;
|
|
2619
|
+
let finalInputStream;
|
|
2620
|
+
const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
|
|
2621
|
+
if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
|
|
2622
|
+
initialReadStream = (0, import_fs2.createReadStream)(path2);
|
|
2623
|
+
finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
|
|
2624
|
+
} else {
|
|
2625
|
+
initialReadStream = (0, import_fs2.createReadStream)(path2);
|
|
2626
|
+
finalInputStream = initialReadStream.pipe(createStripBomTransform());
|
|
2627
|
+
}
|
|
2628
|
+
finalInputStream.pipe(csvStream);
|
|
2629
|
+
try {
|
|
2630
|
+
for await (const row of import_stream2.Readable.from(csvStream)) {
|
|
2631
|
+
if (!isGeneratorConfigArray) {
|
|
2632
|
+
const rowData = Object.keys(fileConfig.fields).reduce((acc, key) => {
|
|
2633
|
+
const keyGenerator = fileConfig.fields[key.trim()];
|
|
2634
|
+
if (!keyGenerator) {
|
|
2635
|
+
return acc;
|
|
2636
|
+
}
|
|
2637
|
+
return {
|
|
2638
|
+
...acc,
|
|
2639
|
+
[keyGenerator.key]: keyGenerator.valueTransformer ? keyGenerator.valueTransformer(row[key]) : row[key]
|
|
2640
|
+
};
|
|
2641
|
+
}, fileConfig.addSyncedAtColumn ? { _wly_synced_at: syncedAt } : {});
|
|
2642
|
+
yield rowData;
|
|
2643
|
+
} else {
|
|
2644
|
+
const rowData = Object.keys(row).reduce((acc, key, i) => {
|
|
2645
|
+
const keyGenerator = fileConfig.fields[i];
|
|
2646
|
+
if (!keyGenerator) {
|
|
2647
|
+
return acc;
|
|
2648
|
+
}
|
|
2649
|
+
return {
|
|
2650
|
+
...acc,
|
|
2651
|
+
[keyGenerator.key]: keyGenerator.valueTransformer ? keyGenerator.valueTransformer(row[i]) : row[i]
|
|
2652
|
+
};
|
|
2653
|
+
}, fileConfig.addSyncedAtColumn ? { _wly_synced_at: syncedAt } : {});
|
|
2654
|
+
yield rowData;
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
} finally {
|
|
2658
|
+
initialReadStream.destroy();
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
var checkCsvHeaderRow = async (path2, fileConfig) => {
|
|
2662
|
+
return new Promise((resolve, reject) => {
|
|
2663
|
+
const colsWithParsingConfig = Object.keys(fileConfig.fields).map((key) => key.trim());
|
|
2664
|
+
console.debug(`${logPrefix} CSV Expected columns:`, colsWithParsingConfig.map((col) => {
|
|
2665
|
+
const info = getStringHexInfo(col);
|
|
2666
|
+
return `"${info.original}" [${info.hex}]`;
|
|
2667
|
+
}));
|
|
2668
|
+
const isGeneratorConfigArray = Array.isArray(fileConfig.fields);
|
|
2669
|
+
const csvOptions2 = { separator: fileConfig.separator };
|
|
2670
|
+
if (isGeneratorConfigArray) {
|
|
2671
|
+
csvOptions2.headers = false;
|
|
2672
|
+
}
|
|
2673
|
+
const csvStream = (0, import_csv_parser.default)(csvOptions2);
|
|
2674
|
+
let initialReadStream;
|
|
2675
|
+
let finalInputStream;
|
|
2676
|
+
const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
|
|
2677
|
+
if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
|
|
2678
|
+
initialReadStream = (0, import_fs2.createReadStream)(path2);
|
|
2679
|
+
finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
|
|
2680
|
+
} else {
|
|
2681
|
+
initialReadStream = (0, import_fs2.createReadStream)(path2);
|
|
2682
|
+
finalInputStream = initialReadStream.pipe(createStripBomTransform());
|
|
2683
|
+
}
|
|
2684
|
+
let isFirstLine = true;
|
|
2685
|
+
finalInputStream.pipe(csvStream).on("data", (row) => {
|
|
2686
|
+
if (isFirstLine) {
|
|
2687
|
+
let headers = [];
|
|
2688
|
+
if (isGeneratorConfigArray) {
|
|
2689
|
+
headers = Object.values(row).filter((r) => !!r);
|
|
2690
|
+
} else {
|
|
2691
|
+
headers = Object.keys(row).filter((r) => !!r);
|
|
2692
|
+
}
|
|
2693
|
+
console.debug(`${logPrefix} CSV Detected headers:`, headers.map((h, idx) => {
|
|
2694
|
+
const info = getStringHexInfo(h);
|
|
2695
|
+
return `[${idx}]: "${info.original}" [${info.hex}]`;
|
|
2696
|
+
}));
|
|
2697
|
+
if (isGeneratorConfigArray) {
|
|
2698
|
+
colsWithParsingConfig.forEach((col, i) => {
|
|
2699
|
+
if (col !== headers[i]) {
|
|
2700
|
+
const hexComparison = formatHexComparison(col, headers[i] || "<undefined>");
|
|
2701
|
+
console.error(`${logPrefix} CSV Header Mismatch:${hexComparison}`);
|
|
2702
|
+
throw new Error((0, import_util3.format)(
|
|
2703
|
+
`CSV header mismatch at position %s: expected '%s' but got '%s' in file %s`,
|
|
2704
|
+
i,
|
|
2705
|
+
col,
|
|
2706
|
+
headers[i] || "<undefined>",
|
|
2707
|
+
path2
|
|
2708
|
+
));
|
|
2709
|
+
}
|
|
2710
|
+
});
|
|
2711
|
+
} else {
|
|
2712
|
+
colsWithParsingConfig.forEach((col) => {
|
|
2713
|
+
if (!headers.includes(col)) {
|
|
2714
|
+
const colInfo = getStringHexInfo(col);
|
|
2715
|
+
console.error(`CSV Missing column: "${col}" [${colInfo.hex}]`);
|
|
2716
|
+
console.error(`Available headers:`, headers.map((h) => `"${h}" [${getStringHexInfo(h).hex}]`));
|
|
2717
|
+
const trimmedMatch = headers.find((h) => h.trim() === col.trim());
|
|
2718
|
+
if (trimmedMatch) {
|
|
2719
|
+
console.error(`Potential trimmed match: "${trimmedMatch}" [${getStringHexInfo(trimmedMatch).hex}]`);
|
|
2720
|
+
}
|
|
2721
|
+
throw new Error((0, import_util3.format)(
|
|
2722
|
+
`CSV missing expected column '%s' in file: %s`,
|
|
2723
|
+
col,
|
|
2724
|
+
path2
|
|
2725
|
+
));
|
|
2726
|
+
}
|
|
2727
|
+
});
|
|
2728
|
+
}
|
|
2729
|
+
isFirstLine = false;
|
|
2730
|
+
} else {
|
|
2731
|
+
initialReadStream.destroy();
|
|
2732
|
+
}
|
|
2733
|
+
}).on("error", (error) => {
|
|
2734
|
+
reject(new Error(`Error processing CSV file: ${error.message}`));
|
|
2735
|
+
});
|
|
2736
|
+
initialReadStream.on("close", () => {
|
|
2737
|
+
resolve();
|
|
2738
|
+
});
|
|
2739
|
+
});
|
|
2740
|
+
};
|
|
2741
|
+
|
|
2742
|
+
// src/sdk/file-processing/csv/writer.ts
|
|
2743
|
+
var import_csv_writer = require("csv-writer");
|
|
2744
|
+
var import_util4 = require("util");
|
|
2745
|
+
var logPrefix2 = "[csvWriter]";
|
|
2746
|
+
var writeDataToCsv = async (fileName, data) => {
|
|
2747
|
+
try {
|
|
2748
|
+
const firstRow = data[0];
|
|
2749
|
+
if (!firstRow) {
|
|
2750
|
+
console.log(`${logPrefix2} No data to write to CSV file=%s`, fileName);
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
const csvWriter = (0, import_csv_writer.createObjectCsvWriter)({
|
|
2754
|
+
path: fileName,
|
|
2755
|
+
header: Object.keys(firstRow).map((col) => {
|
|
2756
|
+
return { id: col, title: col };
|
|
2757
|
+
})
|
|
2758
|
+
});
|
|
2759
|
+
await csvWriter.writeRecords(data);
|
|
2760
|
+
console.log(`${logPrefix2} CSV file=%s written successfully`, fileName);
|
|
2761
|
+
} catch (err) {
|
|
2762
|
+
if (err instanceof Error) {
|
|
2763
|
+
throw new Error((0, import_util4.format)(`error while writing csv file=%s, err: %s`, fileName, err.message));
|
|
2764
|
+
}
|
|
2765
|
+
throw err;
|
|
2766
|
+
}
|
|
2767
|
+
};
|
|
2768
|
+
var writeGeneratorToCSV = async (generator, outputFileName) => {
|
|
2769
|
+
try {
|
|
2770
|
+
let rowCount = 0;
|
|
2771
|
+
const firstDataRow = (await generator.next()).value;
|
|
2772
|
+
if (!firstDataRow) {
|
|
2773
|
+
console.log(`${logPrefix2} [%s] first data row of csv empty, skipping`, outputFileName);
|
|
2774
|
+
return 0;
|
|
2775
|
+
}
|
|
2776
|
+
console.log(`${logPrefix2} [%s] inferring headers from first data row`, outputFileName);
|
|
2777
|
+
const headers = Object.keys(firstDataRow).map((col) => {
|
|
2778
|
+
return {
|
|
2779
|
+
id: col,
|
|
2780
|
+
title: col
|
|
2781
|
+
};
|
|
2782
|
+
});
|
|
2783
|
+
const csvWriter = (0, import_csv_writer.createObjectCsvWriter)({
|
|
2784
|
+
path: outputFileName,
|
|
2785
|
+
header: headers,
|
|
2786
|
+
alwaysQuote: true
|
|
2787
|
+
});
|
|
2788
|
+
console.log(`${logPrefix2} [%s] writing csv`, outputFileName);
|
|
2789
|
+
let batch = [firstDataRow];
|
|
2790
|
+
for await (const data of generator) {
|
|
2791
|
+
batch.push(data);
|
|
2792
|
+
rowCount++;
|
|
2793
|
+
if (batch.length > 1e4) {
|
|
2794
|
+
try {
|
|
2795
|
+
await csvWriter.writeRecords(batch);
|
|
2796
|
+
} catch (err) {
|
|
2797
|
+
if (err instanceof Error) {
|
|
2798
|
+
throw new Error(
|
|
2799
|
+
(0, import_util4.format)(
|
|
2800
|
+
`error while writing batch to csv batch (first 3 records)=%j, err: %s`,
|
|
2801
|
+
batch.slice(0, 3),
|
|
2802
|
+
err.message
|
|
2803
|
+
)
|
|
2804
|
+
);
|
|
2805
|
+
}
|
|
2806
|
+
throw err;
|
|
2807
|
+
}
|
|
2808
|
+
batch = [];
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
await csvWriter.writeRecords(batch);
|
|
2812
|
+
console.log(`${logPrefix2} [%s] csv written successfully with %d rows`, outputFileName, rowCount);
|
|
2813
|
+
return rowCount;
|
|
2814
|
+
} catch (err) {
|
|
2815
|
+
if (err instanceof Error) {
|
|
2816
|
+
throw new Error((0, import_util4.format)(`error while writing csv file=%s, err: %s`, outputFileName, err.message));
|
|
2817
|
+
}
|
|
2818
|
+
throw err;
|
|
2819
|
+
}
|
|
2820
|
+
};
|
|
2821
|
+
|
|
2822
|
+
// src/sdk/file-processing/file-stream.ts
|
|
2823
|
+
var import_xlsx2 = __toESM(require("xlsx"));
|
|
2824
|
+
var FileStream = class extends Stream {
|
|
2825
|
+
localFilePaths;
|
|
2826
|
+
constructor(config, localFilePath, tapState, target) {
|
|
2827
|
+
super(config, tapState, target);
|
|
2828
|
+
this.localFilePaths = Array.isArray(localFilePath) ? localFilePath : [localFilePath];
|
|
2829
|
+
this.streamId = config.streamId;
|
|
2830
|
+
this.primaryKey = config.primaryKeys;
|
|
2831
|
+
this.replicationMethod = config.replicationMethod;
|
|
2832
|
+
}
|
|
2833
|
+
/**
|
|
2834
|
+
* Build JSON Schema from the file config's field definitions.
|
|
2835
|
+
*/
|
|
2836
|
+
async getSchema() {
|
|
2837
|
+
if (this.config.format === "EXCEL" /* EXCEL */) {
|
|
2838
|
+
const excelConfig = this.config.excel;
|
|
2839
|
+
if (excelConfig.type === "single-sheet-extraction") {
|
|
2840
|
+
const jsonSchema = excelFieldsToJsonSchema(excelConfig.columns);
|
|
2841
|
+
return { jsonSchema };
|
|
2842
|
+
}
|
|
2843
|
+
return void 0;
|
|
2844
|
+
}
|
|
2845
|
+
if (this.config.format === "CSV" /* CSV */) {
|
|
2846
|
+
const jsonSchema = csvFieldsToJsonSchema(this.config.csv.fields);
|
|
2847
|
+
return { jsonSchema };
|
|
2848
|
+
}
|
|
2849
|
+
throw new Error(`FileStream: No valid format config for stream ${this.streamId}`);
|
|
2850
|
+
}
|
|
2851
|
+
/**
|
|
2852
|
+
* Yield records from all file(s).
|
|
2853
|
+
* When multiple file paths are provided, records are yielded sequentially from each file.
|
|
2854
|
+
*/
|
|
2855
|
+
async *_getRecords() {
|
|
2856
|
+
for (const filePath of this.localFilePaths) {
|
|
2857
|
+
if (this.config.format === "EXCEL" /* EXCEL */) {
|
|
2858
|
+
const data = await extractExcelRows(filePath, this.config.excel);
|
|
2859
|
+
for (const row of data) {
|
|
2860
|
+
yield row;
|
|
2861
|
+
}
|
|
2862
|
+
} else if (this.config.format === "CSV" /* CSV */) {
|
|
2863
|
+
yield* rowGeneratorFromCsv(filePath, this.config.csv);
|
|
2864
|
+
} else {
|
|
2865
|
+
throw new Error(`FileStream: Unsupported format for stream ${this.streamId}`);
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
};
|
|
2870
|
+
function createExcelStreamConfig(excelConfig, fileName, localFilePath) {
|
|
2871
|
+
if (excelConfig.type === "single-sheet-extraction") {
|
|
2872
|
+
const primaryKeys = extractPrimaryKeysFromExcelFields(excelConfig.columns);
|
|
2873
|
+
let tableName2;
|
|
2874
|
+
if (typeof excelConfig.tableName === "function") {
|
|
2875
|
+
const variables = excelConfig.fileNameVariablesExtractor(fileName);
|
|
2876
|
+
tableName2 = excelConfig.tableName(fileName, void 0, variables);
|
|
2877
|
+
} else {
|
|
2878
|
+
tableName2 = excelConfig.tableName;
|
|
2879
|
+
}
|
|
2880
|
+
return {
|
|
2881
|
+
format: "EXCEL" /* EXCEL */,
|
|
2882
|
+
streamId: tableName2,
|
|
2883
|
+
replicationMethod: excelConfig.replicationMethod,
|
|
2884
|
+
primaryKeys,
|
|
2885
|
+
excel: excelConfig
|
|
2886
|
+
};
|
|
2887
|
+
}
|
|
2888
|
+
let tableName;
|
|
2889
|
+
if (typeof excelConfig.tableName === "function") {
|
|
2890
|
+
if (!localFilePath) {
|
|
2891
|
+
throw new Error("localFilePath is required for processor configs with dynamic table names");
|
|
2892
|
+
}
|
|
2893
|
+
const workbook = import_xlsx2.default.readFile(localFilePath, { dense: true });
|
|
2894
|
+
const variables = excelConfig.fileNameVariablesExtractor(fileName, workbook);
|
|
2895
|
+
tableName = excelConfig.tableName(fileName, workbook, variables);
|
|
2896
|
+
} else {
|
|
2897
|
+
tableName = excelConfig.tableName;
|
|
2898
|
+
}
|
|
2899
|
+
return {
|
|
2900
|
+
format: "EXCEL" /* EXCEL */,
|
|
2901
|
+
streamId: tableName,
|
|
2902
|
+
replicationMethod: excelConfig.replicationMethod,
|
|
2903
|
+
primaryKeys: [],
|
|
2904
|
+
excel: excelConfig
|
|
2905
|
+
};
|
|
2906
|
+
}
|
|
2907
|
+
function createCsvStreamConfig(streamId, csvConfig, options) {
|
|
2908
|
+
return {
|
|
2909
|
+
format: "CSV" /* CSV */,
|
|
2910
|
+
streamId,
|
|
2911
|
+
replicationMethod: options?.replicationMethod ?? "FULL_TABLE" /* FULL_TABLE */,
|
|
2912
|
+
primaryKeys: options?.primaryKeys ?? extractPrimaryKeysFromCsvConfig(csvConfig.fields),
|
|
2913
|
+
csv: csvConfig
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2916
|
+
async function processFileStreams(entries, tapState, target) {
|
|
2917
|
+
for (const entry of entries) {
|
|
2918
|
+
const stream = new FileStream(entry.config, entry.filePath, tapState, target);
|
|
2919
|
+
await stream.sync();
|
|
2920
|
+
}
|
|
2921
|
+
await target.complete();
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
// src/sdk/file-processing/csv/config-builder.ts
|
|
2925
|
+
var CsvExtractionConfigBuilder = class _CsvExtractionConfigBuilder {
|
|
2926
|
+
_separator = ",";
|
|
2927
|
+
_encoding;
|
|
2928
|
+
_addSyncedAtColumn;
|
|
2929
|
+
_fields;
|
|
2930
|
+
_fieldsMode;
|
|
2931
|
+
_replicationMethod;
|
|
2932
|
+
_primaryKeys;
|
|
2933
|
+
static create() {
|
|
2934
|
+
return new _CsvExtractionConfigBuilder();
|
|
2935
|
+
}
|
|
2936
|
+
separator(sep) {
|
|
2937
|
+
this._separator = sep;
|
|
2938
|
+
return this;
|
|
2939
|
+
}
|
|
2940
|
+
encoding(enc) {
|
|
2941
|
+
this._encoding = enc;
|
|
2942
|
+
return this;
|
|
2943
|
+
}
|
|
2944
|
+
addSyncedAtColumn(enabled = true) {
|
|
2945
|
+
this._addSyncedAtColumn = enabled;
|
|
2946
|
+
return this;
|
|
2947
|
+
}
|
|
2948
|
+
fieldsFromDict(mapping) {
|
|
2949
|
+
if (this._fieldsMode === "array") {
|
|
2950
|
+
throw new Error("Cannot use fieldsFromDict after fieldsFromArray");
|
|
2951
|
+
}
|
|
2952
|
+
this._fieldsMode = "dict";
|
|
2953
|
+
this._fields = mapping;
|
|
2954
|
+
return this;
|
|
2955
|
+
}
|
|
2956
|
+
fieldsFromArray(fields) {
|
|
2957
|
+
if (this._fieldsMode === "dict") {
|
|
2958
|
+
throw new Error("Cannot use fieldsFromArray after fieldsFromDict");
|
|
2959
|
+
}
|
|
2960
|
+
this._fieldsMode = "array";
|
|
2961
|
+
this._fields = fields;
|
|
2962
|
+
return this;
|
|
2963
|
+
}
|
|
2964
|
+
replicationMethod(method) {
|
|
2965
|
+
this._replicationMethod = method;
|
|
2966
|
+
return this;
|
|
2967
|
+
}
|
|
2968
|
+
primaryKeys(keys) {
|
|
2969
|
+
this._primaryKeys = keys;
|
|
2970
|
+
return this;
|
|
2971
|
+
}
|
|
2972
|
+
build() {
|
|
2973
|
+
if (!this._fields) {
|
|
2974
|
+
throw new Error("Fields must be configured (use fieldsFromDict or fieldsFromArray)");
|
|
2975
|
+
}
|
|
2976
|
+
if (Array.isArray(this._fields) && this._fields.length === 0) {
|
|
2977
|
+
throw new Error("Fields must not be empty");
|
|
2978
|
+
}
|
|
2979
|
+
if (!Array.isArray(this._fields) && Object.keys(this._fields).length === 0) {
|
|
2980
|
+
throw new Error("Fields must not be empty");
|
|
2981
|
+
}
|
|
2982
|
+
const config = {
|
|
2983
|
+
separator: this._separator,
|
|
2984
|
+
fields: this._fields
|
|
2985
|
+
};
|
|
2986
|
+
if (this._encoding !== void 0) {
|
|
2987
|
+
config.encoding = this._encoding;
|
|
2988
|
+
}
|
|
2989
|
+
if (this._addSyncedAtColumn !== void 0) {
|
|
2990
|
+
config.addSyncedAtColumn = this._addSyncedAtColumn;
|
|
2991
|
+
}
|
|
2992
|
+
return config;
|
|
2993
|
+
}
|
|
2994
|
+
buildStreamConfig(streamId) {
|
|
2995
|
+
const csvConfig = this.build();
|
|
2996
|
+
const options = {};
|
|
2997
|
+
if (this._replicationMethod !== void 0) {
|
|
2998
|
+
options.replicationMethod = this._replicationMethod;
|
|
2999
|
+
}
|
|
3000
|
+
if (this._primaryKeys !== void 0) {
|
|
3001
|
+
options.primaryKeys = this._primaryKeys;
|
|
3002
|
+
}
|
|
3003
|
+
return createCsvStreamConfig(streamId, csvConfig, options);
|
|
3004
|
+
}
|
|
3005
|
+
};
|
|
3006
|
+
|
|
3007
|
+
// src/sdk/file-processing/file-tap.ts
|
|
3008
|
+
var FileTap = class _FileTap extends Tap {
|
|
3009
|
+
entries;
|
|
3010
|
+
constructor(target, config, stateProvider, entries) {
|
|
3011
|
+
super(target, config, stateProvider);
|
|
3012
|
+
this.entries = entries;
|
|
3013
|
+
}
|
|
3014
|
+
/**
|
|
3015
|
+
* Convenience constructor for the common case where all configs share the same file path.
|
|
3016
|
+
*/
|
|
3017
|
+
static fromConfigs(target, config, stateProvider, fileConfigs, sharedFilePath) {
|
|
3018
|
+
const entries = fileConfigs.map((c) => ({
|
|
3019
|
+
config: c,
|
|
3020
|
+
filePath: sharedFilePath
|
|
3021
|
+
}));
|
|
3022
|
+
return new _FileTap(target, config, stateProvider, entries);
|
|
3023
|
+
}
|
|
3024
|
+
async init() {
|
|
3025
|
+
for (const entry of this.entries) {
|
|
3026
|
+
const stream = new FileStream(
|
|
3027
|
+
entry.config,
|
|
3028
|
+
entry.filePath,
|
|
3029
|
+
this.tapState,
|
|
3030
|
+
this.target
|
|
3031
|
+
);
|
|
3032
|
+
this.streams.push(stream);
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
};
|
|
3036
|
+
|
|
3037
|
+
// src/sdk/file-processing/file-patterns.ts
|
|
3038
|
+
var VariableExtractors = class {
|
|
3039
|
+
/**
|
|
3040
|
+
* Returns the filename as a variable.
|
|
3041
|
+
*/
|
|
3042
|
+
static filename() {
|
|
3043
|
+
return (filename) => ({
|
|
3044
|
+
fileName: filename
|
|
3045
|
+
});
|
|
3046
|
+
}
|
|
3047
|
+
/**
|
|
3048
|
+
* Extracts named capture groups from a regex pattern.
|
|
3049
|
+
*/
|
|
3050
|
+
static regex(pattern) {
|
|
3051
|
+
return (filename) => {
|
|
3052
|
+
const match = filename.match(pattern);
|
|
3053
|
+
return match?.groups ?? {};
|
|
3054
|
+
};
|
|
3055
|
+
}
|
|
3056
|
+
/**
|
|
3057
|
+
* Combines multiple extraction functions.
|
|
3058
|
+
*/
|
|
3059
|
+
static combine(...extractors) {
|
|
3060
|
+
return (filename) => {
|
|
3061
|
+
return extractors.reduce((acc, extractor) => {
|
|
3062
|
+
return { ...acc, ...extractor(filename) };
|
|
3063
|
+
}, {});
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
};
|
|
3067
|
+
var FilePatterns = class {
|
|
3068
|
+
/**
|
|
3069
|
+
* Creates a validator for files starting with a specific prefix (case-insensitive).
|
|
3070
|
+
*/
|
|
3071
|
+
static startsWith(prefix) {
|
|
3072
|
+
return (filename) => filename.toLowerCase().startsWith(prefix.toLowerCase());
|
|
3073
|
+
}
|
|
3074
|
+
/**
|
|
3075
|
+
* Creates a validator for files matching a regex pattern.
|
|
3076
|
+
*/
|
|
3077
|
+
static regex(pattern) {
|
|
3078
|
+
return (filename) => pattern.test(filename);
|
|
3079
|
+
}
|
|
3080
|
+
/**
|
|
3081
|
+
* Combines multiple validators with AND logic.
|
|
3082
|
+
*/
|
|
3083
|
+
static and(...validators) {
|
|
3084
|
+
return (filename) => validators.every((validator) => validator(filename));
|
|
3085
|
+
}
|
|
3086
|
+
/**
|
|
3087
|
+
* Combines multiple validators with OR logic.
|
|
3088
|
+
*/
|
|
3089
|
+
static or(...validators) {
|
|
3090
|
+
return (filename) => validators.some((validator) => validator(filename));
|
|
3091
|
+
}
|
|
3092
|
+
};
|
|
3093
|
+
|
|
3094
|
+
// src/services/sftp.ts
|
|
3095
|
+
var import_ssh2_sftp_client = __toESM(require("ssh2-sftp-client"));
|
|
3096
|
+
var import_ssh2_sftp_client2 = __toESM(require("ssh2-sftp-client"));
|
|
3097
|
+
var SftpService = new import_ssh2_sftp_client.default();
|
|
3098
|
+
|
|
3099
|
+
// src/services/cloud-storage.ts
|
|
3100
|
+
var import_storage = require("@google-cloud/storage");
|
|
3101
|
+
var import_util5 = require("util");
|
|
3102
|
+
var pathModule = __toESM(require("path"));
|
|
3103
|
+
var import_fs3 = require("fs");
|
|
3104
|
+
|
|
3105
|
+
// node_modules/uuid/dist/esm-node/rng.js
|
|
3106
|
+
var import_crypto = __toESM(require("crypto"));
|
|
3107
|
+
var rnds8Pool = new Uint8Array(256);
|
|
3108
|
+
var poolPtr = rnds8Pool.length;
|
|
3109
|
+
function rng() {
|
|
3110
|
+
if (poolPtr > rnds8Pool.length - 16) {
|
|
3111
|
+
import_crypto.default.randomFillSync(rnds8Pool);
|
|
3112
|
+
poolPtr = 0;
|
|
3113
|
+
}
|
|
3114
|
+
return rnds8Pool.slice(poolPtr, poolPtr += 16);
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
// node_modules/uuid/dist/esm-node/regex.js
|
|
3118
|
+
var regex_default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;
|
|
3119
|
+
|
|
3120
|
+
// node_modules/uuid/dist/esm-node/validate.js
|
|
3121
|
+
function validate(uuid) {
|
|
3122
|
+
return typeof uuid === "string" && regex_default.test(uuid);
|
|
3123
|
+
}
|
|
3124
|
+
var validate_default = validate;
|
|
3125
|
+
|
|
3126
|
+
// node_modules/uuid/dist/esm-node/stringify.js
|
|
3127
|
+
var byteToHex = [];
|
|
3128
|
+
for (let i = 0; i < 256; ++i) {
|
|
3129
|
+
byteToHex.push((i + 256).toString(16).substr(1));
|
|
3130
|
+
}
|
|
3131
|
+
function stringify3(arr, offset = 0) {
|
|
3132
|
+
const uuid = (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
|
|
3133
|
+
if (!validate_default(uuid)) {
|
|
3134
|
+
throw TypeError("Stringified UUID is invalid");
|
|
3135
|
+
}
|
|
3136
|
+
return uuid;
|
|
3137
|
+
}
|
|
3138
|
+
var stringify_default = stringify3;
|
|
3139
|
+
|
|
3140
|
+
// node_modules/uuid/dist/esm-node/v4.js
|
|
3141
|
+
function v4(options, buf, offset) {
|
|
3142
|
+
options = options || {};
|
|
3143
|
+
const rnds = options.random || (options.rng || rng)();
|
|
3144
|
+
rnds[6] = rnds[6] & 15 | 64;
|
|
3145
|
+
rnds[8] = rnds[8] & 63 | 128;
|
|
3146
|
+
if (buf) {
|
|
3147
|
+
offset = offset || 0;
|
|
3148
|
+
for (let i = 0; i < 16; ++i) {
|
|
3149
|
+
buf[offset + i] = rnds[i];
|
|
3150
|
+
}
|
|
3151
|
+
return buf;
|
|
3152
|
+
}
|
|
3153
|
+
return stringify_default(rnds);
|
|
3154
|
+
}
|
|
3155
|
+
var v4_default = v4;
|
|
3156
|
+
|
|
3157
|
+
// src/services/cloud-storage.ts
|
|
3158
|
+
var logPrefix3 = "[CloudStorageService]";
|
|
3159
|
+
var tmpDir = "tmp";
|
|
3160
|
+
var CloudStorageService = class {
|
|
3161
|
+
storage;
|
|
3162
|
+
bucket;
|
|
3163
|
+
processedSuffix;
|
|
3164
|
+
supportedExtensions;
|
|
3165
|
+
path;
|
|
3166
|
+
constructor(bucketName, path2, opts) {
|
|
3167
|
+
this.storage = new import_storage.Storage({ retryOptions: { autoRetry: true, maxRetries: 20 } });
|
|
3168
|
+
this.bucket = this.storage.bucket(bucketName);
|
|
3169
|
+
this.path = path2;
|
|
3170
|
+
this.processedSuffix = opts?.processedSuffix ?? ".processed";
|
|
3171
|
+
this.supportedExtensions = opts?.supportedExtensions ?? [];
|
|
3172
|
+
}
|
|
3173
|
+
/**
|
|
3174
|
+
* Lists all files in the bucket with an optional prefix.
|
|
3175
|
+
*/
|
|
3176
|
+
async listFiles(prefix) {
|
|
3177
|
+
const effectivePrefix = prefix || this.path;
|
|
3178
|
+
const [files] = await this.bucket.getFiles(
|
|
3179
|
+
effectivePrefix ? { prefix: effectivePrefix } : void 0
|
|
3180
|
+
);
|
|
3181
|
+
return files.map((f) => f.name);
|
|
3182
|
+
}
|
|
3183
|
+
/**
|
|
3184
|
+
* Returns files that haven't been marked as processed.
|
|
3185
|
+
* Filters by supported extensions if configured.
|
|
3186
|
+
*/
|
|
3187
|
+
async getUnprocessedFiles() {
|
|
3188
|
+
const allFiles = await this.listFiles();
|
|
3189
|
+
const markerFiles = new Set(
|
|
3190
|
+
allFiles.filter((f) => f.endsWith(this.processedSuffix)).map((f) => f.replace(this.processedSuffix, ""))
|
|
3191
|
+
);
|
|
3192
|
+
return allFiles.filter((f) => {
|
|
3193
|
+
if (f.endsWith(this.processedSuffix)) return false;
|
|
3194
|
+
if (f.endsWith("/")) return false;
|
|
3195
|
+
if (this.supportedExtensions.length > 0) {
|
|
3196
|
+
const ext = pathModule.extname(f).toLowerCase();
|
|
3197
|
+
if (!this.supportedExtensions.includes(ext)) return false;
|
|
3198
|
+
}
|
|
3199
|
+
return !markerFiles.has(f);
|
|
3200
|
+
});
|
|
3201
|
+
}
|
|
3202
|
+
/**
|
|
3203
|
+
* Creates a marker file to indicate a file has been processed.
|
|
3204
|
+
*/
|
|
3205
|
+
async createMarkerFile(fileName) {
|
|
3206
|
+
const markerFileName = `${fileName}${this.processedSuffix}`;
|
|
3207
|
+
try {
|
|
3208
|
+
const file = this.bucket.file(markerFileName);
|
|
3209
|
+
await file.save((0, import_util5.format)("Marked file %s as processed", fileName));
|
|
3210
|
+
logger.info(`${logPrefix3} Marker file ${markerFileName} created successfully.`);
|
|
3211
|
+
} catch (err) {
|
|
3212
|
+
if (err instanceof Error) {
|
|
3213
|
+
throw new Error((0, import_util5.format)(`error while writing marker file=%s, err:%s`, markerFileName, err.message));
|
|
3214
|
+
}
|
|
3215
|
+
throw err;
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
/**
|
|
3219
|
+
* Downloads a file from GCS to a local tmp directory.
|
|
3220
|
+
* Returns the local path of the downloaded file.
|
|
3221
|
+
*/
|
|
3222
|
+
async downloadFile(filePath, fileName) {
|
|
3223
|
+
const file = this.bucket.file(filePath);
|
|
3224
|
+
const tmpDirPath = pathModule.join(process.cwd(), tmpDir);
|
|
3225
|
+
if (!(0, import_fs3.existsSync)(tmpDirPath)) {
|
|
3226
|
+
(0, import_fs3.mkdirSync)(tmpDirPath);
|
|
3227
|
+
}
|
|
3228
|
+
const destFilename = pathModule.join(process.cwd(), tmpDir, fileName);
|
|
3229
|
+
try {
|
|
3230
|
+
if ((0, import_fs3.existsSync)(destFilename)) {
|
|
3231
|
+
(0, import_fs3.unlinkSync)(destFilename);
|
|
3232
|
+
}
|
|
3233
|
+
await file.download({ destination: destFilename });
|
|
3234
|
+
logger.info(`${logPrefix3} Downloaded ${fileName} to ${destFilename}`);
|
|
3235
|
+
return destFilename;
|
|
3236
|
+
} catch (err) {
|
|
3237
|
+
if (err instanceof Error) {
|
|
3238
|
+
throw new Error((0, import_util5.format)(
|
|
3239
|
+
`can't download file=%s from bucket, err:%s`,
|
|
3240
|
+
fileName,
|
|
3241
|
+
err.message
|
|
3242
|
+
));
|
|
3243
|
+
}
|
|
3244
|
+
throw err;
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
/**
|
|
3248
|
+
* Uploads a local file to GCS.
|
|
3249
|
+
*/
|
|
3250
|
+
async uploadFile(localPath, destPath) {
|
|
3251
|
+
try {
|
|
3252
|
+
logger.info(`${logPrefix3} preparing to upload '%s' to '%s'`, localPath, destPath);
|
|
3253
|
+
const [file] = await this.bucket.upload(localPath, { destination: destPath });
|
|
3254
|
+
logger.info(`${logPrefix3} file %s has been successfully uploaded into %s`, localPath, destPath);
|
|
3255
|
+
return file;
|
|
3256
|
+
} catch (err) {
|
|
3257
|
+
if (err instanceof Error) {
|
|
3258
|
+
throw new Error((0, import_util5.format)(`error while uploading file file=%s into path=%s, err:%s`, localPath, destPath, err.message));
|
|
3259
|
+
}
|
|
3260
|
+
throw err;
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
/**
|
|
3264
|
+
* Reads a GCS object and returns its contents as a UTF-8 string.
|
|
3265
|
+
*/
|
|
3266
|
+
async readObjectAsString(objectPath) {
|
|
3267
|
+
try {
|
|
3268
|
+
await this.bucket.get({ autoCreate: false });
|
|
3269
|
+
const fileRef = this.bucket.file(objectPath);
|
|
3270
|
+
const [exists] = await fileRef.exists();
|
|
3271
|
+
if (!exists) {
|
|
3272
|
+
throw new Error(`GCS object not found: gs://${this.bucket.name}/${objectPath}`);
|
|
3273
|
+
}
|
|
3274
|
+
const [contents] = await fileRef.download();
|
|
3275
|
+
return contents.toString("utf8");
|
|
3276
|
+
} catch (err) {
|
|
3277
|
+
throw new Error((0, import_util5.format)(`error reading GCS object gs://${this.bucket.name}/${objectPath}, err: %s`, err?.message));
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
/**
|
|
3281
|
+
* Writes a string to a GCS object with JSON content type.
|
|
3282
|
+
*/
|
|
3283
|
+
async writeStringObject(objectPath, contents) {
|
|
3284
|
+
try {
|
|
3285
|
+
await this.bucket.get({ autoCreate: false });
|
|
3286
|
+
const fileRef = this.bucket.file(objectPath);
|
|
3287
|
+
await fileRef.save(contents, { contentType: "application/json" });
|
|
3288
|
+
logger.info(`Uploaded object to gs://${this.bucket.name}/${objectPath}`);
|
|
3289
|
+
} catch (err) {
|
|
3290
|
+
throw new Error((0, import_util5.format)(`error writing GCS object gs://${this.bucket.name}/${objectPath}, err: %s`, err?.message));
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
/**
|
|
3294
|
+
* Uploads a local file to the bucket with a unique name based on prefix, streamId, and UUID.
|
|
3295
|
+
* Returns the GCS File reference.
|
|
3296
|
+
*/
|
|
3297
|
+
async uploadFileWithUniqueName(filePath, prefix, streamId) {
|
|
3298
|
+
try {
|
|
3299
|
+
await this.bucket.get({ autoCreate: false });
|
|
3300
|
+
const destinationFileName = `${prefix}/${streamId}-${v4_default()}.jsonnl`;
|
|
3301
|
+
await this.bucket.upload(filePath, {
|
|
3302
|
+
destination: destinationFileName
|
|
3303
|
+
});
|
|
3304
|
+
logger.info(`Uploaded ${filePath} into ${this.bucket.name}/${destinationFileName} GCS File`);
|
|
3305
|
+
return this.bucket.file(destinationFileName);
|
|
3306
|
+
} catch (err) {
|
|
3307
|
+
logger.error(`Issue when uploading file into GCS bucket for stream: ${streamId}
|
|
3308
|
+
|
|
3309
|
+
Error: ${err.message}
|
|
3310
|
+
Stack: ${err.stack}
|
|
3311
|
+
Code: ${err.code}
|
|
3312
|
+
`);
|
|
3313
|
+
throw err;
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
};
|
|
3317
|
+
|
|
3318
|
+
// src/services/zip.ts
|
|
3319
|
+
var fse = __toESM(require("fs-extra"));
|
|
3320
|
+
var import_decompress = __toESM(require("decompress"));
|
|
3321
|
+
var unzip = async (zipFilePath, extractedPath) => {
|
|
3322
|
+
await fse.ensureDir(extractedPath);
|
|
3323
|
+
await (0, import_decompress.default)(zipFilePath, extractedPath);
|
|
3324
|
+
return extractedPath;
|
|
3325
|
+
};
|
|
3326
|
+
|
|
2224
3327
|
// src/targets/bigquery/helpers.ts
|
|
2225
3328
|
var safeColumnName = (key) => {
|
|
2226
3329
|
let returnString = key;
|
|
@@ -2257,8 +3360,8 @@ var import_decimal = __toESM(require("decimal.js"));
|
|
|
2257
3360
|
var import_dayjs5 = __toESM(require("dayjs"));
|
|
2258
3361
|
var validateDateRange = (record, schema) => {
|
|
2259
3362
|
Object.keys(schema).forEach((key) => {
|
|
2260
|
-
const { format:
|
|
2261
|
-
if (
|
|
3363
|
+
const { format: format8 } = schema[key];
|
|
3364
|
+
if (format8 === "date-time") {
|
|
2262
3365
|
if (record[key]) {
|
|
2263
3366
|
const formattedDate = (0, import_dayjs5.default)(record[key]).format(defaultDateTimeFormat);
|
|
2264
3367
|
const isNotTooMuchInTheFuture = (0, import_dayjs5.default)(record[key]).isBefore((0, import_dayjs5.default)("9999-12-31 23:59:59.999999"));
|
|
@@ -2342,96 +3445,6 @@ var BqClientHolder = class {
|
|
|
2342
3445
|
}
|
|
2343
3446
|
};
|
|
2344
3447
|
|
|
2345
|
-
// src/targets/bigquery/service/cloudStorage.ts
|
|
2346
|
-
var import_storage = require("@google-cloud/storage");
|
|
2347
|
-
|
|
2348
|
-
// node_modules/uuid/dist/esm-node/rng.js
|
|
2349
|
-
var import_crypto = __toESM(require("crypto"));
|
|
2350
|
-
var rnds8Pool = new Uint8Array(256);
|
|
2351
|
-
var poolPtr = rnds8Pool.length;
|
|
2352
|
-
function rng() {
|
|
2353
|
-
if (poolPtr > rnds8Pool.length - 16) {
|
|
2354
|
-
import_crypto.default.randomFillSync(rnds8Pool);
|
|
2355
|
-
poolPtr = 0;
|
|
2356
|
-
}
|
|
2357
|
-
return rnds8Pool.slice(poolPtr, poolPtr += 16);
|
|
2358
|
-
}
|
|
2359
|
-
|
|
2360
|
-
// node_modules/uuid/dist/esm-node/regex.js
|
|
2361
|
-
var regex_default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;
|
|
2362
|
-
|
|
2363
|
-
// node_modules/uuid/dist/esm-node/validate.js
|
|
2364
|
-
function validate(uuid) {
|
|
2365
|
-
return typeof uuid === "string" && regex_default.test(uuid);
|
|
2366
|
-
}
|
|
2367
|
-
var validate_default = validate;
|
|
2368
|
-
|
|
2369
|
-
// node_modules/uuid/dist/esm-node/stringify.js
|
|
2370
|
-
var byteToHex = [];
|
|
2371
|
-
for (let i = 0; i < 256; ++i) {
|
|
2372
|
-
byteToHex.push((i + 256).toString(16).substr(1));
|
|
2373
|
-
}
|
|
2374
|
-
function stringify3(arr, offset = 0) {
|
|
2375
|
-
const uuid = (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
|
|
2376
|
-
if (!validate_default(uuid)) {
|
|
2377
|
-
throw TypeError("Stringified UUID is invalid");
|
|
2378
|
-
}
|
|
2379
|
-
return uuid;
|
|
2380
|
-
}
|
|
2381
|
-
var stringify_default = stringify3;
|
|
2382
|
-
|
|
2383
|
-
// node_modules/uuid/dist/esm-node/v4.js
|
|
2384
|
-
function v4(options, buf, offset) {
|
|
2385
|
-
options = options || {};
|
|
2386
|
-
const rnds = options.random || (options.rng || rng)();
|
|
2387
|
-
rnds[6] = rnds[6] & 15 | 64;
|
|
2388
|
-
rnds[8] = rnds[8] & 63 | 128;
|
|
2389
|
-
if (buf) {
|
|
2390
|
-
offset = offset || 0;
|
|
2391
|
-
for (let i = 0; i < 16; ++i) {
|
|
2392
|
-
buf[offset + i] = rnds[i];
|
|
2393
|
-
}
|
|
2394
|
-
return buf;
|
|
2395
|
-
}
|
|
2396
|
-
return stringify_default(rnds);
|
|
2397
|
-
}
|
|
2398
|
-
var v4_default = v4;
|
|
2399
|
-
|
|
2400
|
-
// src/targets/bigquery/service/cloudStorage.ts
|
|
2401
|
-
var uploadFileInBucket = async (streamId, gcsBuckerName, connectorId, filePath) => {
|
|
2402
|
-
try {
|
|
2403
|
-
const retryOptions = { autoRetry: true, maxRetries: 20 };
|
|
2404
|
-
let storage = new import_storage.Storage({ retryOptions });
|
|
2405
|
-
const bucketRef = storage.bucket(gcsBuckerName);
|
|
2406
|
-
await bucketRef.get({ autoCreate: false });
|
|
2407
|
-
const destinationFileName = `${connectorId}/${streamId}-${v4_default()}.jsonnl`;
|
|
2408
|
-
await bucketRef.upload(filePath, {
|
|
2409
|
-
destination: destinationFileName
|
|
2410
|
-
});
|
|
2411
|
-
logger.info(`\u{1F5F3} Uploaded ${filePath} into ${gcsBuckerName}/${destinationFileName} GCS File`);
|
|
2412
|
-
return bucketRef.file(destinationFileName);
|
|
2413
|
-
} catch (err) {
|
|
2414
|
-
logger.error(`Issue when uploading file into GCS bucket for stream: ${streamId}
|
|
2415
|
-
|
|
2416
|
-
Error: ${err.message}
|
|
2417
|
-
Stack: ${err.stack}
|
|
2418
|
-
Code: ${err.code}
|
|
2419
|
-
`);
|
|
2420
|
-
if (err.code < 500) {
|
|
2421
|
-
await haltAndCatchFire(
|
|
2422
|
-
`unauthorized`,
|
|
2423
|
-
`We couldn't connect to your Cloud Storage bucket \u{1F614}
|
|
2424
|
-
|
|
2425
|
-
The error from Google is: '${err.message}' \u{1F440}
|
|
2426
|
-
|
|
2427
|
-
Could you troubleshoot your Cloud Storage configuration in Google Cloud and sync again the source? \u{1F64F}`,
|
|
2428
|
-
`Got error from cloud storage lib`
|
|
2429
|
-
);
|
|
2430
|
-
}
|
|
2431
|
-
throw err;
|
|
2432
|
-
}
|
|
2433
|
-
};
|
|
2434
|
-
|
|
2435
3448
|
// src/targets/bigquery/service/dbSync.ts
|
|
2436
3449
|
var import_chunk = __toESM(require("lodash/chunk.js"));
|
|
2437
3450
|
var import_async_mutex2 = require("async-mutex");
|
|
@@ -2472,21 +3485,21 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
|
|
|
2472
3485
|
safeColumnName = safeColumnName;
|
|
2473
3486
|
getWarehouseTypeFromJSONSchema = (definition) => {
|
|
2474
3487
|
const type = definition.type;
|
|
2475
|
-
const
|
|
3488
|
+
const format8 = definition.format;
|
|
2476
3489
|
let typeArr;
|
|
2477
3490
|
if (type instanceof Array) {
|
|
2478
3491
|
typeArr = type;
|
|
2479
3492
|
} else {
|
|
2480
3493
|
typeArr = [type];
|
|
2481
3494
|
}
|
|
2482
|
-
if (
|
|
2483
|
-
switch (
|
|
3495
|
+
if (format8) {
|
|
3496
|
+
switch (format8) {
|
|
2484
3497
|
case "date-time":
|
|
2485
3498
|
return "TIMESTAMP";
|
|
2486
3499
|
case "json":
|
|
2487
3500
|
return "STRING";
|
|
2488
3501
|
default:
|
|
2489
|
-
throw new Error(`StreamId: ${this.streamId} - Unsupported format: ${
|
|
3502
|
+
throw new Error(`StreamId: ${this.streamId} - Unsupported format: ${format8}`);
|
|
2490
3503
|
}
|
|
2491
3504
|
} else if (typeArr.includes("number")) {
|
|
2492
3505
|
return "NUMERIC";
|
|
@@ -2582,6 +3595,13 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
|
|
|
2582
3595
|
...this.getMergeQueries()
|
|
2583
3596
|
];
|
|
2584
3597
|
};
|
|
3598
|
+
getAppendQueries = () => {
|
|
3599
|
+
const columnUnsafeToSafeMapping = this.renamedColumnStore.getUnsafeToSafeColumnMapping(this.streamId);
|
|
3600
|
+
const columns = Object.keys(columnUnsafeToSafeMapping).map((unsafeKey) => `\`${columnUnsafeToSafeMapping[unsafeKey]}\``).join(`, `);
|
|
3601
|
+
return [
|
|
3602
|
+
`INSERT INTO ${this.sqlTableId} (${columns}) SELECT ${columns} FROM ${this.sqlStagingTableId};`
|
|
3603
|
+
];
|
|
3604
|
+
};
|
|
2585
3605
|
createDatabaseAndSchemaIfNotExists = async (retryCount) => {
|
|
2586
3606
|
try {
|
|
2587
3607
|
await semaphore2.runExclusive(async () => {
|
|
@@ -2706,11 +3726,11 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
|
|
|
2706
3726
|
};
|
|
2707
3727
|
loadStreamInStagingArea = async (localFilePath) => {
|
|
2708
3728
|
try {
|
|
2709
|
-
const
|
|
2710
|
-
|
|
2711
|
-
|
|
3729
|
+
const gcsService = new CloudStorageService(this.config.loading_deck_gcs_bucket_name);
|
|
3730
|
+
const file = await gcsService.uploadFileWithUniqueName(
|
|
3731
|
+
localFilePath,
|
|
2712
3732
|
this.config.connector_id,
|
|
2713
|
-
|
|
3733
|
+
this.streamId
|
|
2714
3734
|
);
|
|
2715
3735
|
const metadata = {
|
|
2716
3736
|
sourceFormat: "NEWLINE_DELIMITED_JSON",
|
|
@@ -2719,6 +3739,17 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
|
|
|
2719
3739
|
await this.loadGCSFileInTable(this.sqlStagingTableId, file, metadata);
|
|
2720
3740
|
} catch (err) {
|
|
2721
3741
|
logger.error(`StreamId: ${this.streamId} - Error while uploading stream.`);
|
|
3742
|
+
if (err.code < 500) {
|
|
3743
|
+
await haltAndCatchFire(
|
|
3744
|
+
`unauthorized`,
|
|
3745
|
+
`We couldn't connect to your Cloud Storage bucket \u{1F614}
|
|
3746
|
+
|
|
3747
|
+
The error from Google is: '${err.message}' \u{1F440}
|
|
3748
|
+
|
|
3749
|
+
Could you troubleshoot your Cloud Storage configuration in Google Cloud and sync again the source? \u{1F64F}`,
|
|
3750
|
+
`Got error from cloud storage lib`
|
|
3751
|
+
);
|
|
3752
|
+
}
|
|
2722
3753
|
throw err;
|
|
2723
3754
|
}
|
|
2724
3755
|
};
|
|
@@ -2846,47 +3877,16 @@ var BigQueryTarget = class extends ITarget {
|
|
|
2846
3877
|
convertNumberIntoDecimal = convertNumberIntoDecimal;
|
|
2847
3878
|
};
|
|
2848
3879
|
|
|
2849
|
-
// src/sdk/service/gcs.ts
|
|
2850
|
-
var import_storage2 = require("@google-cloud/storage");
|
|
2851
|
-
var import_util3 = require("util");
|
|
2852
|
-
var getStorage = () => new import_storage2.Storage({ retryOptions: { autoRetry: true, maxRetries: 20 } });
|
|
2853
|
-
var readObjectAsString = async (bucketName, objectPath) => {
|
|
2854
|
-
try {
|
|
2855
|
-
const storage = getStorage();
|
|
2856
|
-
const bucketRef = storage.bucket(bucketName);
|
|
2857
|
-
await bucketRef.get({ autoCreate: false });
|
|
2858
|
-
const fileRef = bucketRef.file(objectPath);
|
|
2859
|
-
const [exists] = await fileRef.exists();
|
|
2860
|
-
if (!exists) {
|
|
2861
|
-
throw new Error(`GCS object not found: gs://${bucketName}/${objectPath}`);
|
|
2862
|
-
}
|
|
2863
|
-
const [contents] = await fileRef.download();
|
|
2864
|
-
return contents.toString("utf8");
|
|
2865
|
-
} catch (err) {
|
|
2866
|
-
throw new Error((0, import_util3.format)(`error reading GCS object gs://${bucketName}/${objectPath}, err: %s`, err?.message));
|
|
2867
|
-
}
|
|
2868
|
-
};
|
|
2869
|
-
var writeStringObject = async (bucketName, objectPath, contents) => {
|
|
2870
|
-
try {
|
|
2871
|
-
const storage = getStorage();
|
|
2872
|
-
const bucketRef = storage.bucket(bucketName);
|
|
2873
|
-
await bucketRef.get({ autoCreate: false });
|
|
2874
|
-
const fileRef = bucketRef.file(objectPath);
|
|
2875
|
-
await fileRef.save(contents, { contentType: "application/json" });
|
|
2876
|
-
logger.info(`\u{1F5F3} Uploaded object to gs://${bucketName}/${objectPath}`);
|
|
2877
|
-
} catch (err) {
|
|
2878
|
-
throw new Error((0, import_util3.format)(`error writing GCS object gs://${bucketName}/${objectPath}, err: %s`, err?.message));
|
|
2879
|
-
}
|
|
2880
|
-
};
|
|
2881
|
-
|
|
2882
3880
|
// src/state-providers/gcs/main.ts
|
|
2883
|
-
var
|
|
3881
|
+
var import_util6 = require("util");
|
|
2884
3882
|
var GCSStateProvider = class {
|
|
2885
3883
|
bucketName;
|
|
2886
3884
|
connectorId;
|
|
3885
|
+
service;
|
|
2887
3886
|
constructor(connectorId, bucketName) {
|
|
2888
3887
|
this.connectorId = connectorId;
|
|
2889
3888
|
this.bucketName = bucketName;
|
|
3889
|
+
this.service = new CloudStorageService(bucketName);
|
|
2890
3890
|
}
|
|
2891
3891
|
getObjectPath() {
|
|
2892
3892
|
return `${this.connectorId}/state.json`;
|
|
@@ -2895,11 +3895,11 @@ var GCSStateProvider = class {
|
|
|
2895
3895
|
const objectPath = this.getObjectPath();
|
|
2896
3896
|
try {
|
|
2897
3897
|
logger.info(`\u{1F4C1} Loading state from gs://${this.bucketName}/${objectPath}`);
|
|
2898
|
-
const raw = await readObjectAsString(
|
|
3898
|
+
const raw = await this.service.readObjectAsString(objectPath);
|
|
2899
3899
|
const parsed = JSON.parse(raw);
|
|
2900
3900
|
return { state: parsed };
|
|
2901
3901
|
} catch (err) {
|
|
2902
|
-
logger.warn((0,
|
|
3902
|
+
logger.warn((0, import_util6.format)(`error loading state from gs://${this.bucketName}/${objectPath}, will start with empty state, err: %s`, err?.message));
|
|
2903
3903
|
return { state: void 0 };
|
|
2904
3904
|
}
|
|
2905
3905
|
}
|
|
@@ -2907,10 +3907,10 @@ var GCSStateProvider = class {
|
|
|
2907
3907
|
const objectPath = this.getObjectPath();
|
|
2908
3908
|
try {
|
|
2909
3909
|
logger.info(`\u{1F4DD} Writing state to gs://${this.bucketName}/${objectPath}`);
|
|
2910
|
-
await writeStringObject(
|
|
3910
|
+
await this.service.writeStringObject(objectPath, state);
|
|
2911
3911
|
} catch (err) {
|
|
2912
|
-
logger.error((0,
|
|
2913
|
-
throw new Error((0,
|
|
3912
|
+
logger.error((0, import_util6.format)(`\u{1F4A5} Error while writing state to gs://${this.bucketName}/${objectPath}, err: %s`, err?.message));
|
|
3913
|
+
throw new Error((0, import_util6.format)(`error writing state to gs://${this.bucketName}/${objectPath}, err: %s`, err?.message));
|
|
2914
3914
|
}
|
|
2915
3915
|
}
|
|
2916
3916
|
};
|
|
@@ -2921,9 +3921,16 @@ var GCSStateProvider = class {
|
|
|
2921
3921
|
Authenticator,
|
|
2922
3922
|
BATCH_INTERVAL_MS,
|
|
2923
3923
|
BigQueryTarget,
|
|
3924
|
+
CloudStorageService,
|
|
2924
3925
|
CounterMetric,
|
|
3926
|
+
CsvExtractionConfigBuilder,
|
|
2925
3927
|
DEFAULT_MAX_CONCURRENT_STREAMS,
|
|
2926
3928
|
EXECUTION_TIME_METRIC_NAME,
|
|
3929
|
+
ExcelExtractionConfigBuilder,
|
|
3930
|
+
FileFormat,
|
|
3931
|
+
FilePatterns,
|
|
3932
|
+
FileStream,
|
|
3933
|
+
FileTap,
|
|
2927
3934
|
GCSStateProvider,
|
|
2928
3935
|
ITarget,
|
|
2929
3936
|
MissingFieldInSchemaError,
|
|
@@ -2936,16 +3943,33 @@ var GCSStateProvider = class {
|
|
|
2936
3943
|
ReplicationMethodMessage,
|
|
2937
3944
|
SchemaMessage,
|
|
2938
3945
|
SchemaValidationError,
|
|
3946
|
+
SftpClient,
|
|
3947
|
+
SftpService,
|
|
2939
3948
|
StateMessage,
|
|
2940
3949
|
StateService,
|
|
2941
3950
|
Stream,
|
|
2942
3951
|
StreamWarehouseSyncService,
|
|
2943
3952
|
Tap,
|
|
3953
|
+
VariableExtractors,
|
|
2944
3954
|
_greaterThan,
|
|
2945
3955
|
addWhalyFields,
|
|
3956
|
+
checkCsvHeaderRow,
|
|
3957
|
+
createCellReference,
|
|
3958
|
+
createCsvStreamConfig,
|
|
3959
|
+
createExcelGenerator,
|
|
3960
|
+
createExcelStreamConfig,
|
|
2946
3961
|
createTemporaryFileStream,
|
|
3962
|
+
csvFieldsToJsonSchema,
|
|
3963
|
+
excelColumnToIndex,
|
|
3964
|
+
excelFieldsToJsonSchema,
|
|
3965
|
+
excelRowToIndex,
|
|
3966
|
+
extractExcelRows,
|
|
3967
|
+
extractPrimaryKeysFromCsvConfig,
|
|
3968
|
+
extractPrimaryKeysFromExcelFields,
|
|
2947
3969
|
extractStateForStream,
|
|
3970
|
+
fieldTypeToJsonSchema,
|
|
2948
3971
|
finalizeStateProgressMarkers,
|
|
3972
|
+
findAllMatchingConfigs,
|
|
2949
3973
|
flattenSchema,
|
|
2950
3974
|
getAllMetrics,
|
|
2951
3975
|
getAxiosInstance,
|
|
@@ -2958,14 +3982,23 @@ var GCSStateProvider = class {
|
|
|
2958
3982
|
gracefulExit,
|
|
2959
3983
|
haltAndCatchFire,
|
|
2960
3984
|
incrementStreamState,
|
|
3985
|
+
indexToExcelColumn,
|
|
3986
|
+
indexToExcelRow,
|
|
2961
3987
|
loadJson,
|
|
2962
3988
|
loadSchema,
|
|
2963
3989
|
logger,
|
|
3990
|
+
parseCellReference,
|
|
2964
3991
|
postFormDataApiCall,
|
|
2965
3992
|
postJSONApiCall,
|
|
2966
3993
|
postUrlEncodedApiCall,
|
|
2967
3994
|
printMemoryFootprint,
|
|
3995
|
+
processFileStreams,
|
|
2968
3996
|
removeParasiteProperties,
|
|
2969
|
-
|
|
3997
|
+
rowGeneratorFromCsv,
|
|
3998
|
+
safePath,
|
|
3999
|
+
unzip,
|
|
4000
|
+
validateExcelConfig,
|
|
4001
|
+
writeDataToCsv,
|
|
4002
|
+
writeGeneratorToCSV
|
|
2970
4003
|
});
|
|
2971
4004
|
//# sourceMappingURL=index.js.map
|