@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/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
- safePath: () => safePath
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 path = `./tmp/${safePath(streamId)}-${(0, import_uniqueId.default)()}.tmp`;
1775
- const writeStream = import_fs_extra.default.createWriteStream(path, { flags: "w" });
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(path) {
1782
- let formattedPath = path;
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
- if (message.keyProperties.length === 0) {
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
- if (stream.getReplicationMethod() === "INCREMENTAL" && batchedRecords > 1e6) {
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: format6 } = schema[key];
2261
- if (format6 === "date-time") {
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 format6 = definition.format;
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 (format6) {
2483
- switch (format6) {
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: ${format6}`);
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 file = await uploadFileInBucket(
2710
- this.streamId,
2711
- this.config.loading_deck_gcs_bucket_name,
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
- localFilePath
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 import_util4 = require("util");
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(this.bucketName, objectPath);
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, import_util4.format)(`error loading state from gs://${this.bucketName}/${objectPath}, will start with empty state, err: %s`, err?.message));
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(this.bucketName, objectPath, state);
3910
+ await this.service.writeStringObject(objectPath, state);
2911
3911
  } catch (err) {
2912
- logger.error((0, import_util4.format)(`\u{1F4A5} Error while writing state to gs://${this.bucketName}/${objectPath}, err: %s`, err?.message));
2913
- throw new Error((0, import_util4.format)(`error writing state to gs://${this.bucketName}/${objectPath}, err: %s`, err?.message));
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
- safePath
3997
+ rowGeneratorFromCsv,
3998
+ safePath,
3999
+ unzip,
4000
+ validateExcelConfig,
4001
+ writeDataToCsv,
4002
+ writeGeneratorToCSV
2970
4003
  });
2971
4004
  //# sourceMappingURL=index.js.map