@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.mjs CHANGED
@@ -380,6 +380,7 @@ var haltAndCatchFire = async (errorType, errorText, errorDebugText) => {
380
380
  var ReplicationMethod = /* @__PURE__ */ ((ReplicationMethod2) => {
381
381
  ReplicationMethod2["INCREMENTAL"] = "INCREMENTAL";
382
382
  ReplicationMethod2["FULL_TABLE"] = "FULL_TABLE";
383
+ ReplicationMethod2["APPEND"] = "APPEND";
383
384
  return ReplicationMethod2;
384
385
  })(ReplicationMethod || {});
385
386
 
@@ -1504,6 +1505,8 @@ var StreamWarehouseSyncService = class {
1504
1505
  mergeQueries = this.getReplaceQueries();
1505
1506
  } else if (replicationMethod === "INCREMENTAL") {
1506
1507
  mergeQueries = this.getMergeQueries();
1508
+ } else if (replicationMethod === "APPEND") {
1509
+ mergeQueries = this.getAppendQueries();
1507
1510
  } else if (replicationMethod === "LOG_BASED") {
1508
1511
  mergeQueries = this.getMergeQueries();
1509
1512
  }
@@ -1685,15 +1688,15 @@ import fs2 from "fs-extra";
1685
1688
  import uniqueId from "lodash/uniqueId.js";
1686
1689
  var createTemporaryFileStream = (streamId) => {
1687
1690
  fs2.ensureDirSync("./tmp");
1688
- const path = `./tmp/${safePath(streamId)}-${uniqueId()}.tmp`;
1689
- const writeStream = fs2.createWriteStream(path, { flags: "w" });
1691
+ const path2 = `./tmp/${safePath(streamId)}-${uniqueId()}.tmp`;
1692
+ const writeStream = fs2.createWriteStream(path2, { flags: "w" });
1690
1693
  return {
1691
1694
  stream: writeStream,
1692
- path
1695
+ path: path2
1693
1696
  };
1694
1697
  };
1695
- function safePath(path) {
1696
- let formattedPath = path;
1698
+ function safePath(path2) {
1699
+ let formattedPath = path2;
1697
1700
  formattedPath = formattedPath.replace(/^The\s+/gi, "");
1698
1701
  formattedPath = formattedPath.replace(/\s+(?:\(|\[)?Dis(?:c|k) (?:One|Two|Three|Four|Five|Six|Seven|Eight|Nine|Ten|\d)+(?:\)|\])?/gi, "");
1699
1702
  formattedPath = formattedPath.replace(/\s+/gi, "_");
@@ -2002,7 +2005,8 @@ var ITarget = class _ITarget {
2002
2005
  }
2003
2006
  });
2004
2007
  streamState.setSchema(schema);
2005
- if (message.keyProperties.length === 0) {
2008
+ const streamReplicationMethod = this.streams[streamId]?.getReplicationMethod();
2009
+ if (message.keyProperties.length === 0 && streamReplicationMethod !== "APPEND" /* APPEND */) {
2006
2010
  throw new Error(`StreamId: ${message.stream} - \`key_properties\` field is required and can't be empty in SCHEMA message.
2007
2011
  Received SCHEMA message: ${JSON.stringify(message)}`);
2008
2012
  }
@@ -2066,7 +2070,8 @@ var ITarget = class _ITarget {
2066
2070
  return false;
2067
2071
  }
2068
2072
  const batchedRecords = stream.getBatchedRowCount();
2069
- if (stream.getReplicationMethod() === "INCREMENTAL" && batchedRecords > 1e6) {
2073
+ const method = stream.getReplicationMethod();
2074
+ if ((method === "INCREMENTAL" || method === "APPEND") && batchedRecords > 1e6) {
2070
2075
  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.`);
2071
2076
  return true;
2072
2077
  } else {
@@ -2135,6 +2140,1071 @@ var ITarget = class _ITarget {
2135
2140
  };
2136
2141
  };
2137
2142
 
2143
+ // src/sdk/file-processing/types.ts
2144
+ var FileFormat = /* @__PURE__ */ ((FileFormat2) => {
2145
+ FileFormat2["CSV"] = "CSV";
2146
+ FileFormat2["EXCEL"] = "EXCEL";
2147
+ return FileFormat2;
2148
+ })(FileFormat || {});
2149
+
2150
+ // src/sdk/file-processing/schema-utils.ts
2151
+ function fieldTypeToJsonSchema(fieldType) {
2152
+ switch (fieldType) {
2153
+ case "STRING":
2154
+ return { type: ["null", "string"] };
2155
+ case "FLOAT":
2156
+ return { type: ["null", "number"] };
2157
+ case "TIMESTAMP":
2158
+ return { type: ["null", "string"], format: "date-time" };
2159
+ default:
2160
+ return { type: ["null", "string"] };
2161
+ }
2162
+ }
2163
+ function excelFieldsToJsonSchema(fields) {
2164
+ const properties = {};
2165
+ for (const [key, field] of Object.entries(fields)) {
2166
+ properties[key] = fieldTypeToJsonSchema(field.type);
2167
+ }
2168
+ return { type: "object", properties };
2169
+ }
2170
+ function csvFieldsToJsonSchema(config) {
2171
+ const properties = {};
2172
+ if (Array.isArray(config)) {
2173
+ for (const field of config) {
2174
+ properties[field.key] = fieldTypeToJsonSchema(field.type);
2175
+ }
2176
+ } else {
2177
+ for (const [, field] of Object.entries(config)) {
2178
+ properties[field.key] = fieldTypeToJsonSchema(field.type);
2179
+ }
2180
+ }
2181
+ return { type: "object", properties };
2182
+ }
2183
+ function extractPrimaryKeysFromCsvConfig(config) {
2184
+ if (Array.isArray(config)) {
2185
+ return config.map((f) => f.key);
2186
+ }
2187
+ return Object.values(config).map((f) => f.key);
2188
+ }
2189
+ function extractPrimaryKeysFromExcelFields(fields) {
2190
+ return Object.entries(fields).filter(([, field]) => {
2191
+ return field.primaryKey === true || field.primaryKey === true;
2192
+ }).map(([key]) => key);
2193
+ }
2194
+
2195
+ // src/sdk/file-processing/excel/reader.ts
2196
+ import XLSX from "xlsx";
2197
+ import path from "path";
2198
+ var excelColumnToIndex = (columnLetter) => {
2199
+ const letters = columnLetter.toUpperCase();
2200
+ let index = 0;
2201
+ for (let i = 0; i < letters.length; i++) {
2202
+ const charCode = letters.charCodeAt(i);
2203
+ if (charCode < 65 || charCode > 90) {
2204
+ throw new Error(`Invalid column letter: ${columnLetter}`);
2205
+ }
2206
+ index = index * 26 + (charCode - 64);
2207
+ }
2208
+ return index - 1;
2209
+ };
2210
+ var indexToExcelColumn = (columnIndex) => {
2211
+ if (columnIndex < 0) {
2212
+ throw new Error(`Invalid column index: ${columnIndex}. Must be >= 0`);
2213
+ }
2214
+ let result = "";
2215
+ let index = columnIndex;
2216
+ while (true) {
2217
+ result = String.fromCharCode(65 + index % 26) + result;
2218
+ index = Math.floor(index / 26);
2219
+ if (index === 0) break;
2220
+ index--;
2221
+ }
2222
+ return result;
2223
+ };
2224
+ var excelRowToIndex = (rowNumber) => {
2225
+ const rowNum = typeof rowNumber === "string" ? parseInt(rowNumber, 10) : rowNumber;
2226
+ if (isNaN(rowNum) || rowNum < 1) {
2227
+ throw new Error(`Invalid row number: ${rowNumber}. Must be >= 1`);
2228
+ }
2229
+ return rowNum - 1;
2230
+ };
2231
+ var indexToExcelRow = (index) => {
2232
+ if (index < 0) {
2233
+ throw new Error(`Invalid index: ${index}. Must be >= 0`);
2234
+ }
2235
+ return index + 1;
2236
+ };
2237
+ var parseCellReference = (cellRef) => {
2238
+ const match = cellRef.match(/^([A-Z]+)(\d+)$/);
2239
+ if (!match) {
2240
+ throw new Error(`Invalid cell reference: ${cellRef}. Expected format like "A1", "Z24", "AA100"`);
2241
+ }
2242
+ const [, columnLetter, rowNumber] = match;
2243
+ return {
2244
+ columnIndex: excelColumnToIndex(columnLetter),
2245
+ rowIndex: excelRowToIndex(rowNumber)
2246
+ };
2247
+ };
2248
+ var createCellReference = (columnIndex, rowIndex) => {
2249
+ return indexToExcelColumn(columnIndex) + indexToExcelRow(rowIndex);
2250
+ };
2251
+ var extractSingleSheetRows = async (fileName, conf) => {
2252
+ console.log("Extracting single sheet rows from file: %s", fileName);
2253
+ const workbook = XLSX.readFile(fileName, { dense: true });
2254
+ const sheetKeys = Object.keys(workbook.Sheets);
2255
+ const sheet = conf.sheetName ? workbook.Sheets[conf.sheetName] : workbook.Sheets[sheetKeys[0]];
2256
+ if (!sheet) {
2257
+ throw new Error(`Sheet ${conf.sheetName} not found in workbook`);
2258
+ }
2259
+ const sheetData = sheet["!data"];
2260
+ if (!sheetData) {
2261
+ throw new Error(`No data in ${conf.sheetName || "first"} sheet`);
2262
+ }
2263
+ if (sheetData.length === 0) {
2264
+ throw new Error(`Sheet ${conf.sheetName || "first"} is empty`);
2265
+ }
2266
+ const variables = conf.fileNameVariablesExtractor(fileName, workbook);
2267
+ const data = [];
2268
+ let rowIdx = 0;
2269
+ const minColumnIndex = Object.values(conf.columns).reduce((min, column) => {
2270
+ return column.column ? Math.min(min, excelColumnToIndex(column.column)) : min;
2271
+ }, Number.MAX_SAFE_INTEGER);
2272
+ for (const row of sheetData) {
2273
+ if (rowIdx < conf.numberOfRowsToSkip) {
2274
+ rowIdx++;
2275
+ continue;
2276
+ }
2277
+ if (!row) {
2278
+ console.log("Empty row at index: %s, skipped processing.", rowIdx);
2279
+ continue;
2280
+ }
2281
+ if (minColumnIndex !== Number.MAX_SAFE_INTEGER && !row[minColumnIndex]?.v) {
2282
+ console.log("stopping processing because of empty value in the first data column (idx: %s)", minColumnIndex);
2283
+ break;
2284
+ }
2285
+ const rowData = Object.entries(conf.columns).reduce((acc, [key, column]) => {
2286
+ if (column.variableName) {
2287
+ if (!variables) {
2288
+ throw new Error(`No variables extracted from filename, cannot extract derived field ${key}`);
2289
+ }
2290
+ acc[key] = variables[column.variableName] || "";
2291
+ } else {
2292
+ const colIndex = excelColumnToIndex(column.column);
2293
+ acc[key] = row[colIndex]?.v || "";
2294
+ }
2295
+ return acc;
2296
+ }, {});
2297
+ data.push(rowData);
2298
+ rowIdx++;
2299
+ }
2300
+ console.log("Extracted %s rows from sheet %s", data.length, conf.sheetName || "first");
2301
+ return data;
2302
+ };
2303
+ var extractExcelRows = async (localFilePath, conf) => {
2304
+ let data;
2305
+ if (conf.type === "single-sheet-extraction") {
2306
+ data = await extractSingleSheetRows(localFilePath, conf);
2307
+ } else if (conf.type === "processor") {
2308
+ const workbook = XLSX.readFile(localFilePath, { dense: true });
2309
+ data = await conf.processor(workbook);
2310
+ } else {
2311
+ throw new Error(`Unsupported configuration type: ${conf.type}`);
2312
+ }
2313
+ return data;
2314
+ };
2315
+ var validateExcelConfig = (config) => {
2316
+ if (!config.extension) {
2317
+ throw new Error("Extension is required");
2318
+ }
2319
+ if (!config.tableName) {
2320
+ throw new Error("Table name is required");
2321
+ }
2322
+ if (!config.fileNameValidator || typeof config.fileNameValidator !== "function") {
2323
+ throw new Error("File name validator function is required");
2324
+ }
2325
+ if (!config.fileNameVariablesExtractor || typeof config.fileNameVariablesExtractor !== "function") {
2326
+ throw new Error("File name variables extractor function is required");
2327
+ }
2328
+ if (config.type === "single-sheet-extraction" && (!config.columns || Object.keys(config.columns).length === 0)) {
2329
+ throw new Error("At least one column configuration is required for single-sheet-extraction");
2330
+ }
2331
+ if (config.type === "single-sheet-extraction") {
2332
+ Object.entries(config.columns).forEach(([key, column]) => {
2333
+ if (!column.type) {
2334
+ throw new Error(`Column ${key} must have a type`);
2335
+ }
2336
+ if (column.column) {
2337
+ const colLetter = column.column;
2338
+ if (!/^[A-Z]+$/i.test(colLetter)) {
2339
+ throw new Error(`Invalid column letter format: ${colLetter}`);
2340
+ }
2341
+ } else if (column.variableName) {
2342
+ } else {
2343
+ throw new Error(`Column ${key} must specify either 'column' or 'variableName'`);
2344
+ }
2345
+ });
2346
+ if (config.numberOfRowsToSkip === void 0 || config.numberOfRowsToSkip < 0) {
2347
+ throw new Error("Number of rows to skip must be specified and non-negative");
2348
+ }
2349
+ } else if (config.type === "processor") {
2350
+ if (!config.processor || typeof config.processor !== "function") {
2351
+ throw new Error("Processor function is required for processor type");
2352
+ }
2353
+ }
2354
+ };
2355
+ var findAllMatchingConfigs = (filename, configs) => {
2356
+ const fileInfo = path.parse(filename);
2357
+ return configs.filter(
2358
+ (config) => config.fileNameValidator(fileInfo.base) && config.extension === fileInfo.ext.replace(".", "")
2359
+ );
2360
+ };
2361
+ async function* createExcelGenerator(data) {
2362
+ for (const row of data) {
2363
+ yield row;
2364
+ }
2365
+ }
2366
+
2367
+ // src/sdk/file-processing/excel/config-builder.ts
2368
+ var ExcelExtractionConfigBuilder = class _ExcelExtractionConfigBuilder {
2369
+ config = {
2370
+ fileNameValidator: () => true,
2371
+ fileNameVariablesExtractor: () => ({}),
2372
+ replicationMethod: "FULL_TABLE" /* FULL_TABLE */
2373
+ };
2374
+ static create() {
2375
+ return new _ExcelExtractionConfigBuilder();
2376
+ }
2377
+ extension(ext) {
2378
+ this.config.extension = ext;
2379
+ return this;
2380
+ }
2381
+ tableName(name) {
2382
+ this.config.tableName = name;
2383
+ return this;
2384
+ }
2385
+ fileValidator(validator) {
2386
+ this.config.fileNameValidator = validator;
2387
+ return this;
2388
+ }
2389
+ variablesExtractor(extractor) {
2390
+ this.config.fileNameVariablesExtractor = extractor;
2391
+ return this;
2392
+ }
2393
+ replicationMethod(method) {
2394
+ this.config.replicationMethod = method;
2395
+ return this;
2396
+ }
2397
+ columns(cols) {
2398
+ if (this.config.type === "single-sheet-extraction") {
2399
+ this.config.columns = cols;
2400
+ } else {
2401
+ throw new Error("Columns can only be set for single-sheet-extraction");
2402
+ }
2403
+ return this;
2404
+ }
2405
+ singleSheet(sheetName, skipRows = 0) {
2406
+ this.config.type = "single-sheet-extraction";
2407
+ if (sheetName !== void 0) {
2408
+ this.config.sheetName = sheetName;
2409
+ }
2410
+ this.config.numberOfRowsToSkip = skipRows;
2411
+ return this;
2412
+ }
2413
+ processor(processorFn) {
2414
+ this.config.type = "processor";
2415
+ this.config.processor = processorFn;
2416
+ return this;
2417
+ }
2418
+ build() {
2419
+ if (!this.config.type) {
2420
+ throw new Error("Configuration type must be specified (singleSheet or processor)");
2421
+ }
2422
+ if (!this.config.extension) {
2423
+ throw new Error("Extension must be set");
2424
+ }
2425
+ if (!this.config.tableName) {
2426
+ throw new Error("Table name must be set");
2427
+ }
2428
+ if (!this.config.replicationMethod) {
2429
+ throw new Error("Replication method must be set");
2430
+ }
2431
+ if (typeof this.config.fileNameValidator !== "function") {
2432
+ throw new Error("fileNameValidator must be a function");
2433
+ }
2434
+ if (typeof this.config.fileNameVariablesExtractor !== "function") {
2435
+ throw new Error("fileNameVariablesExtractor must be a function");
2436
+ }
2437
+ if (this.config.type === "single-sheet-extraction") {
2438
+ const singleSheetConfig = this.config;
2439
+ if (!singleSheetConfig.columns || Object.keys(singleSheetConfig.columns).length === 0) {
2440
+ throw new Error("Columns must be set with at least one column for single-sheet-extraction");
2441
+ }
2442
+ }
2443
+ if (this.config.type === "processor") {
2444
+ const processorConfig = this.config;
2445
+ if (typeof processorConfig.processor !== "function") {
2446
+ throw new Error("Processor function must be set for processor configs");
2447
+ }
2448
+ }
2449
+ return this.config;
2450
+ }
2451
+ };
2452
+
2453
+ // src/sdk/file-processing/csv/reader.ts
2454
+ import csv from "csv-parser";
2455
+ import { createReadStream } from "fs";
2456
+ import { format as format4 } from "util";
2457
+ import { Readable, Transform } from "stream";
2458
+ import * as iconv from "iconv-lite";
2459
+ var logPrefix = "[csvReader]";
2460
+ var createStripBomTransform = () => {
2461
+ let isFirstChunk = true;
2462
+ return new Transform({
2463
+ transform(chunk2, encoding, callback) {
2464
+ if (isFirstChunk) {
2465
+ isFirstChunk = false;
2466
+ if (chunk2.length >= 3 && chunk2[0] === 239 && chunk2[1] === 187 && chunk2[2] === 191) {
2467
+ chunk2 = chunk2.slice(3);
2468
+ }
2469
+ }
2470
+ callback(null, chunk2);
2471
+ }
2472
+ });
2473
+ };
2474
+ var getStringHexInfo = (str) => {
2475
+ const bytes = Buffer.from(str, "utf8");
2476
+ const hex = bytes.toString("hex").match(/.{2}/g)?.join(" ") || "";
2477
+ return {
2478
+ original: str,
2479
+ length: str.length,
2480
+ hex,
2481
+ trimmed: str.trim()
2482
+ };
2483
+ };
2484
+ var formatHexComparison = (expected, actual) => {
2485
+ const expectedInfo = getStringHexInfo(expected);
2486
+ const actualInfo = getStringHexInfo(actual);
2487
+ return `
2488
+ Expected: "${expected}" (${expectedInfo.length} chars) [${expectedInfo.hex}]
2489
+ Actual: "${actual}" (${actualInfo.length} chars) [${actualInfo.hex}]`;
2490
+ };
2491
+ async function* rowGeneratorFromCsv(path2, fileConfig) {
2492
+ const isGeneratorConfigArray = Array.isArray(fileConfig.fields);
2493
+ const csvOptions = { separator: fileConfig.separator };
2494
+ if (isGeneratorConfigArray) {
2495
+ csvOptions.headers = false;
2496
+ }
2497
+ const csvStream = csv(csvOptions);
2498
+ const syncedAt = (/* @__PURE__ */ new Date()).toISOString();
2499
+ let initialReadStream;
2500
+ let finalInputStream;
2501
+ const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
2502
+ if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
2503
+ initialReadStream = createReadStream(path2);
2504
+ finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
2505
+ } else {
2506
+ initialReadStream = createReadStream(path2);
2507
+ finalInputStream = initialReadStream.pipe(createStripBomTransform());
2508
+ }
2509
+ finalInputStream.pipe(csvStream);
2510
+ try {
2511
+ for await (const row of Readable.from(csvStream)) {
2512
+ if (!isGeneratorConfigArray) {
2513
+ const rowData = Object.keys(fileConfig.fields).reduce((acc, key) => {
2514
+ const keyGenerator = fileConfig.fields[key.trim()];
2515
+ if (!keyGenerator) {
2516
+ return acc;
2517
+ }
2518
+ return {
2519
+ ...acc,
2520
+ [keyGenerator.key]: keyGenerator.valueTransformer ? keyGenerator.valueTransformer(row[key]) : row[key]
2521
+ };
2522
+ }, fileConfig.addSyncedAtColumn ? { _wly_synced_at: syncedAt } : {});
2523
+ yield rowData;
2524
+ } else {
2525
+ const rowData = Object.keys(row).reduce((acc, key, i) => {
2526
+ const keyGenerator = fileConfig.fields[i];
2527
+ if (!keyGenerator) {
2528
+ return acc;
2529
+ }
2530
+ return {
2531
+ ...acc,
2532
+ [keyGenerator.key]: keyGenerator.valueTransformer ? keyGenerator.valueTransformer(row[i]) : row[i]
2533
+ };
2534
+ }, fileConfig.addSyncedAtColumn ? { _wly_synced_at: syncedAt } : {});
2535
+ yield rowData;
2536
+ }
2537
+ }
2538
+ } finally {
2539
+ initialReadStream.destroy();
2540
+ }
2541
+ }
2542
+ var checkCsvHeaderRow = async (path2, fileConfig) => {
2543
+ return new Promise((resolve, reject) => {
2544
+ const colsWithParsingConfig = Object.keys(fileConfig.fields).map((key) => key.trim());
2545
+ console.debug(`${logPrefix} CSV Expected columns:`, colsWithParsingConfig.map((col) => {
2546
+ const info = getStringHexInfo(col);
2547
+ return `"${info.original}" [${info.hex}]`;
2548
+ }));
2549
+ const isGeneratorConfigArray = Array.isArray(fileConfig.fields);
2550
+ const csvOptions2 = { separator: fileConfig.separator };
2551
+ if (isGeneratorConfigArray) {
2552
+ csvOptions2.headers = false;
2553
+ }
2554
+ const csvStream = csv(csvOptions2);
2555
+ let initialReadStream;
2556
+ let finalInputStream;
2557
+ const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
2558
+ if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
2559
+ initialReadStream = createReadStream(path2);
2560
+ finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
2561
+ } else {
2562
+ initialReadStream = createReadStream(path2);
2563
+ finalInputStream = initialReadStream.pipe(createStripBomTransform());
2564
+ }
2565
+ let isFirstLine = true;
2566
+ finalInputStream.pipe(csvStream).on("data", (row) => {
2567
+ if (isFirstLine) {
2568
+ let headers = [];
2569
+ if (isGeneratorConfigArray) {
2570
+ headers = Object.values(row).filter((r) => !!r);
2571
+ } else {
2572
+ headers = Object.keys(row).filter((r) => !!r);
2573
+ }
2574
+ console.debug(`${logPrefix} CSV Detected headers:`, headers.map((h, idx) => {
2575
+ const info = getStringHexInfo(h);
2576
+ return `[${idx}]: "${info.original}" [${info.hex}]`;
2577
+ }));
2578
+ if (isGeneratorConfigArray) {
2579
+ colsWithParsingConfig.forEach((col, i) => {
2580
+ if (col !== headers[i]) {
2581
+ const hexComparison = formatHexComparison(col, headers[i] || "<undefined>");
2582
+ console.error(`${logPrefix} CSV Header Mismatch:${hexComparison}`);
2583
+ throw new Error(format4(
2584
+ `CSV header mismatch at position %s: expected '%s' but got '%s' in file %s`,
2585
+ i,
2586
+ col,
2587
+ headers[i] || "<undefined>",
2588
+ path2
2589
+ ));
2590
+ }
2591
+ });
2592
+ } else {
2593
+ colsWithParsingConfig.forEach((col) => {
2594
+ if (!headers.includes(col)) {
2595
+ const colInfo = getStringHexInfo(col);
2596
+ console.error(`CSV Missing column: "${col}" [${colInfo.hex}]`);
2597
+ console.error(`Available headers:`, headers.map((h) => `"${h}" [${getStringHexInfo(h).hex}]`));
2598
+ const trimmedMatch = headers.find((h) => h.trim() === col.trim());
2599
+ if (trimmedMatch) {
2600
+ console.error(`Potential trimmed match: "${trimmedMatch}" [${getStringHexInfo(trimmedMatch).hex}]`);
2601
+ }
2602
+ throw new Error(format4(
2603
+ `CSV missing expected column '%s' in file: %s`,
2604
+ col,
2605
+ path2
2606
+ ));
2607
+ }
2608
+ });
2609
+ }
2610
+ isFirstLine = false;
2611
+ } else {
2612
+ initialReadStream.destroy();
2613
+ }
2614
+ }).on("error", (error) => {
2615
+ reject(new Error(`Error processing CSV file: ${error.message}`));
2616
+ });
2617
+ initialReadStream.on("close", () => {
2618
+ resolve();
2619
+ });
2620
+ });
2621
+ };
2622
+
2623
+ // src/sdk/file-processing/csv/writer.ts
2624
+ import { createObjectCsvWriter } from "csv-writer";
2625
+ import { format as format5 } from "util";
2626
+ var logPrefix2 = "[csvWriter]";
2627
+ var writeDataToCsv = async (fileName, data) => {
2628
+ try {
2629
+ const firstRow = data[0];
2630
+ if (!firstRow) {
2631
+ console.log(`${logPrefix2} No data to write to CSV file=%s`, fileName);
2632
+ return;
2633
+ }
2634
+ const csvWriter = createObjectCsvWriter({
2635
+ path: fileName,
2636
+ header: Object.keys(firstRow).map((col) => {
2637
+ return { id: col, title: col };
2638
+ })
2639
+ });
2640
+ await csvWriter.writeRecords(data);
2641
+ console.log(`${logPrefix2} CSV file=%s written successfully`, fileName);
2642
+ } catch (err) {
2643
+ if (err instanceof Error) {
2644
+ throw new Error(format5(`error while writing csv file=%s, err: %s`, fileName, err.message));
2645
+ }
2646
+ throw err;
2647
+ }
2648
+ };
2649
+ var writeGeneratorToCSV = async (generator, outputFileName) => {
2650
+ try {
2651
+ let rowCount = 0;
2652
+ const firstDataRow = (await generator.next()).value;
2653
+ if (!firstDataRow) {
2654
+ console.log(`${logPrefix2} [%s] first data row of csv empty, skipping`, outputFileName);
2655
+ return 0;
2656
+ }
2657
+ console.log(`${logPrefix2} [%s] inferring headers from first data row`, outputFileName);
2658
+ const headers = Object.keys(firstDataRow).map((col) => {
2659
+ return {
2660
+ id: col,
2661
+ title: col
2662
+ };
2663
+ });
2664
+ const csvWriter = createObjectCsvWriter({
2665
+ path: outputFileName,
2666
+ header: headers,
2667
+ alwaysQuote: true
2668
+ });
2669
+ console.log(`${logPrefix2} [%s] writing csv`, outputFileName);
2670
+ let batch = [firstDataRow];
2671
+ for await (const data of generator) {
2672
+ batch.push(data);
2673
+ rowCount++;
2674
+ if (batch.length > 1e4) {
2675
+ try {
2676
+ await csvWriter.writeRecords(batch);
2677
+ } catch (err) {
2678
+ if (err instanceof Error) {
2679
+ throw new Error(
2680
+ format5(
2681
+ `error while writing batch to csv batch (first 3 records)=%j, err: %s`,
2682
+ batch.slice(0, 3),
2683
+ err.message
2684
+ )
2685
+ );
2686
+ }
2687
+ throw err;
2688
+ }
2689
+ batch = [];
2690
+ }
2691
+ }
2692
+ await csvWriter.writeRecords(batch);
2693
+ console.log(`${logPrefix2} [%s] csv written successfully with %d rows`, outputFileName, rowCount);
2694
+ return rowCount;
2695
+ } catch (err) {
2696
+ if (err instanceof Error) {
2697
+ throw new Error(format5(`error while writing csv file=%s, err: %s`, outputFileName, err.message));
2698
+ }
2699
+ throw err;
2700
+ }
2701
+ };
2702
+
2703
+ // src/sdk/file-processing/file-stream.ts
2704
+ import XLSX2 from "xlsx";
2705
+ var FileStream = class extends Stream {
2706
+ localFilePaths;
2707
+ constructor(config, localFilePath, tapState, target) {
2708
+ super(config, tapState, target);
2709
+ this.localFilePaths = Array.isArray(localFilePath) ? localFilePath : [localFilePath];
2710
+ this.streamId = config.streamId;
2711
+ this.primaryKey = config.primaryKeys;
2712
+ this.replicationMethod = config.replicationMethod;
2713
+ }
2714
+ /**
2715
+ * Build JSON Schema from the file config's field definitions.
2716
+ */
2717
+ async getSchema() {
2718
+ if (this.config.format === "EXCEL" /* EXCEL */) {
2719
+ const excelConfig = this.config.excel;
2720
+ if (excelConfig.type === "single-sheet-extraction") {
2721
+ const jsonSchema = excelFieldsToJsonSchema(excelConfig.columns);
2722
+ return { jsonSchema };
2723
+ }
2724
+ return void 0;
2725
+ }
2726
+ if (this.config.format === "CSV" /* CSV */) {
2727
+ const jsonSchema = csvFieldsToJsonSchema(this.config.csv.fields);
2728
+ return { jsonSchema };
2729
+ }
2730
+ throw new Error(`FileStream: No valid format config for stream ${this.streamId}`);
2731
+ }
2732
+ /**
2733
+ * Yield records from all file(s).
2734
+ * When multiple file paths are provided, records are yielded sequentially from each file.
2735
+ */
2736
+ async *_getRecords() {
2737
+ for (const filePath of this.localFilePaths) {
2738
+ if (this.config.format === "EXCEL" /* EXCEL */) {
2739
+ const data = await extractExcelRows(filePath, this.config.excel);
2740
+ for (const row of data) {
2741
+ yield row;
2742
+ }
2743
+ } else if (this.config.format === "CSV" /* CSV */) {
2744
+ yield* rowGeneratorFromCsv(filePath, this.config.csv);
2745
+ } else {
2746
+ throw new Error(`FileStream: Unsupported format for stream ${this.streamId}`);
2747
+ }
2748
+ }
2749
+ }
2750
+ };
2751
+ function createExcelStreamConfig(excelConfig, fileName, localFilePath) {
2752
+ if (excelConfig.type === "single-sheet-extraction") {
2753
+ const primaryKeys = extractPrimaryKeysFromExcelFields(excelConfig.columns);
2754
+ let tableName2;
2755
+ if (typeof excelConfig.tableName === "function") {
2756
+ const variables = excelConfig.fileNameVariablesExtractor(fileName);
2757
+ tableName2 = excelConfig.tableName(fileName, void 0, variables);
2758
+ } else {
2759
+ tableName2 = excelConfig.tableName;
2760
+ }
2761
+ return {
2762
+ format: "EXCEL" /* EXCEL */,
2763
+ streamId: tableName2,
2764
+ replicationMethod: excelConfig.replicationMethod,
2765
+ primaryKeys,
2766
+ excel: excelConfig
2767
+ };
2768
+ }
2769
+ let tableName;
2770
+ if (typeof excelConfig.tableName === "function") {
2771
+ if (!localFilePath) {
2772
+ throw new Error("localFilePath is required for processor configs with dynamic table names");
2773
+ }
2774
+ const workbook = XLSX2.readFile(localFilePath, { dense: true });
2775
+ const variables = excelConfig.fileNameVariablesExtractor(fileName, workbook);
2776
+ tableName = excelConfig.tableName(fileName, workbook, variables);
2777
+ } else {
2778
+ tableName = excelConfig.tableName;
2779
+ }
2780
+ return {
2781
+ format: "EXCEL" /* EXCEL */,
2782
+ streamId: tableName,
2783
+ replicationMethod: excelConfig.replicationMethod,
2784
+ primaryKeys: [],
2785
+ excel: excelConfig
2786
+ };
2787
+ }
2788
+ function createCsvStreamConfig(streamId, csvConfig, options) {
2789
+ return {
2790
+ format: "CSV" /* CSV */,
2791
+ streamId,
2792
+ replicationMethod: options?.replicationMethod ?? "FULL_TABLE" /* FULL_TABLE */,
2793
+ primaryKeys: options?.primaryKeys ?? extractPrimaryKeysFromCsvConfig(csvConfig.fields),
2794
+ csv: csvConfig
2795
+ };
2796
+ }
2797
+ async function processFileStreams(entries, tapState, target) {
2798
+ for (const entry of entries) {
2799
+ const stream = new FileStream(entry.config, entry.filePath, tapState, target);
2800
+ await stream.sync();
2801
+ }
2802
+ await target.complete();
2803
+ }
2804
+
2805
+ // src/sdk/file-processing/csv/config-builder.ts
2806
+ var CsvExtractionConfigBuilder = class _CsvExtractionConfigBuilder {
2807
+ _separator = ",";
2808
+ _encoding;
2809
+ _addSyncedAtColumn;
2810
+ _fields;
2811
+ _fieldsMode;
2812
+ _replicationMethod;
2813
+ _primaryKeys;
2814
+ static create() {
2815
+ return new _CsvExtractionConfigBuilder();
2816
+ }
2817
+ separator(sep) {
2818
+ this._separator = sep;
2819
+ return this;
2820
+ }
2821
+ encoding(enc) {
2822
+ this._encoding = enc;
2823
+ return this;
2824
+ }
2825
+ addSyncedAtColumn(enabled = true) {
2826
+ this._addSyncedAtColumn = enabled;
2827
+ return this;
2828
+ }
2829
+ fieldsFromDict(mapping) {
2830
+ if (this._fieldsMode === "array") {
2831
+ throw new Error("Cannot use fieldsFromDict after fieldsFromArray");
2832
+ }
2833
+ this._fieldsMode = "dict";
2834
+ this._fields = mapping;
2835
+ return this;
2836
+ }
2837
+ fieldsFromArray(fields) {
2838
+ if (this._fieldsMode === "dict") {
2839
+ throw new Error("Cannot use fieldsFromArray after fieldsFromDict");
2840
+ }
2841
+ this._fieldsMode = "array";
2842
+ this._fields = fields;
2843
+ return this;
2844
+ }
2845
+ replicationMethod(method) {
2846
+ this._replicationMethod = method;
2847
+ return this;
2848
+ }
2849
+ primaryKeys(keys) {
2850
+ this._primaryKeys = keys;
2851
+ return this;
2852
+ }
2853
+ build() {
2854
+ if (!this._fields) {
2855
+ throw new Error("Fields must be configured (use fieldsFromDict or fieldsFromArray)");
2856
+ }
2857
+ if (Array.isArray(this._fields) && this._fields.length === 0) {
2858
+ throw new Error("Fields must not be empty");
2859
+ }
2860
+ if (!Array.isArray(this._fields) && Object.keys(this._fields).length === 0) {
2861
+ throw new Error("Fields must not be empty");
2862
+ }
2863
+ const config = {
2864
+ separator: this._separator,
2865
+ fields: this._fields
2866
+ };
2867
+ if (this._encoding !== void 0) {
2868
+ config.encoding = this._encoding;
2869
+ }
2870
+ if (this._addSyncedAtColumn !== void 0) {
2871
+ config.addSyncedAtColumn = this._addSyncedAtColumn;
2872
+ }
2873
+ return config;
2874
+ }
2875
+ buildStreamConfig(streamId) {
2876
+ const csvConfig = this.build();
2877
+ const options = {};
2878
+ if (this._replicationMethod !== void 0) {
2879
+ options.replicationMethod = this._replicationMethod;
2880
+ }
2881
+ if (this._primaryKeys !== void 0) {
2882
+ options.primaryKeys = this._primaryKeys;
2883
+ }
2884
+ return createCsvStreamConfig(streamId, csvConfig, options);
2885
+ }
2886
+ };
2887
+
2888
+ // src/sdk/file-processing/file-tap.ts
2889
+ var FileTap = class _FileTap extends Tap {
2890
+ entries;
2891
+ constructor(target, config, stateProvider, entries) {
2892
+ super(target, config, stateProvider);
2893
+ this.entries = entries;
2894
+ }
2895
+ /**
2896
+ * Convenience constructor for the common case where all configs share the same file path.
2897
+ */
2898
+ static fromConfigs(target, config, stateProvider, fileConfigs, sharedFilePath) {
2899
+ const entries = fileConfigs.map((c) => ({
2900
+ config: c,
2901
+ filePath: sharedFilePath
2902
+ }));
2903
+ return new _FileTap(target, config, stateProvider, entries);
2904
+ }
2905
+ async init() {
2906
+ for (const entry of this.entries) {
2907
+ const stream = new FileStream(
2908
+ entry.config,
2909
+ entry.filePath,
2910
+ this.tapState,
2911
+ this.target
2912
+ );
2913
+ this.streams.push(stream);
2914
+ }
2915
+ }
2916
+ };
2917
+
2918
+ // src/sdk/file-processing/file-patterns.ts
2919
+ var VariableExtractors = class {
2920
+ /**
2921
+ * Returns the filename as a variable.
2922
+ */
2923
+ static filename() {
2924
+ return (filename) => ({
2925
+ fileName: filename
2926
+ });
2927
+ }
2928
+ /**
2929
+ * Extracts named capture groups from a regex pattern.
2930
+ */
2931
+ static regex(pattern) {
2932
+ return (filename) => {
2933
+ const match = filename.match(pattern);
2934
+ return match?.groups ?? {};
2935
+ };
2936
+ }
2937
+ /**
2938
+ * Combines multiple extraction functions.
2939
+ */
2940
+ static combine(...extractors) {
2941
+ return (filename) => {
2942
+ return extractors.reduce((acc, extractor) => {
2943
+ return { ...acc, ...extractor(filename) };
2944
+ }, {});
2945
+ };
2946
+ }
2947
+ };
2948
+ var FilePatterns = class {
2949
+ /**
2950
+ * Creates a validator for files starting with a specific prefix (case-insensitive).
2951
+ */
2952
+ static startsWith(prefix) {
2953
+ return (filename) => filename.toLowerCase().startsWith(prefix.toLowerCase());
2954
+ }
2955
+ /**
2956
+ * Creates a validator for files matching a regex pattern.
2957
+ */
2958
+ static regex(pattern) {
2959
+ return (filename) => pattern.test(filename);
2960
+ }
2961
+ /**
2962
+ * Combines multiple validators with AND logic.
2963
+ */
2964
+ static and(...validators) {
2965
+ return (filename) => validators.every((validator) => validator(filename));
2966
+ }
2967
+ /**
2968
+ * Combines multiple validators with OR logic.
2969
+ */
2970
+ static or(...validators) {
2971
+ return (filename) => validators.some((validator) => validator(filename));
2972
+ }
2973
+ };
2974
+
2975
+ // src/services/sftp.ts
2976
+ import Client from "ssh2-sftp-client";
2977
+ import { default as default2 } from "ssh2-sftp-client";
2978
+ var SftpService = new Client();
2979
+
2980
+ // src/services/cloud-storage.ts
2981
+ import { Storage } from "@google-cloud/storage";
2982
+ import { format as format6 } from "util";
2983
+ import * as pathModule from "path";
2984
+ import { existsSync, mkdirSync, unlinkSync as unlinkSync2 } from "fs";
2985
+
2986
+ // node_modules/uuid/dist/esm-node/rng.js
2987
+ import crypto from "crypto";
2988
+ var rnds8Pool = new Uint8Array(256);
2989
+ var poolPtr = rnds8Pool.length;
2990
+ function rng() {
2991
+ if (poolPtr > rnds8Pool.length - 16) {
2992
+ crypto.randomFillSync(rnds8Pool);
2993
+ poolPtr = 0;
2994
+ }
2995
+ return rnds8Pool.slice(poolPtr, poolPtr += 16);
2996
+ }
2997
+
2998
+ // node_modules/uuid/dist/esm-node/regex.js
2999
+ 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;
3000
+
3001
+ // node_modules/uuid/dist/esm-node/validate.js
3002
+ function validate(uuid) {
3003
+ return typeof uuid === "string" && regex_default.test(uuid);
3004
+ }
3005
+ var validate_default = validate;
3006
+
3007
+ // node_modules/uuid/dist/esm-node/stringify.js
3008
+ var byteToHex = [];
3009
+ for (let i = 0; i < 256; ++i) {
3010
+ byteToHex.push((i + 256).toString(16).substr(1));
3011
+ }
3012
+ function stringify3(arr, offset = 0) {
3013
+ 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();
3014
+ if (!validate_default(uuid)) {
3015
+ throw TypeError("Stringified UUID is invalid");
3016
+ }
3017
+ return uuid;
3018
+ }
3019
+ var stringify_default = stringify3;
3020
+
3021
+ // node_modules/uuid/dist/esm-node/v4.js
3022
+ function v4(options, buf, offset) {
3023
+ options = options || {};
3024
+ const rnds = options.random || (options.rng || rng)();
3025
+ rnds[6] = rnds[6] & 15 | 64;
3026
+ rnds[8] = rnds[8] & 63 | 128;
3027
+ if (buf) {
3028
+ offset = offset || 0;
3029
+ for (let i = 0; i < 16; ++i) {
3030
+ buf[offset + i] = rnds[i];
3031
+ }
3032
+ return buf;
3033
+ }
3034
+ return stringify_default(rnds);
3035
+ }
3036
+ var v4_default = v4;
3037
+
3038
+ // src/services/cloud-storage.ts
3039
+ var logPrefix3 = "[CloudStorageService]";
3040
+ var tmpDir = "tmp";
3041
+ var CloudStorageService = class {
3042
+ storage;
3043
+ bucket;
3044
+ processedSuffix;
3045
+ supportedExtensions;
3046
+ path;
3047
+ constructor(bucketName, path2, opts) {
3048
+ this.storage = new Storage({ retryOptions: { autoRetry: true, maxRetries: 20 } });
3049
+ this.bucket = this.storage.bucket(bucketName);
3050
+ this.path = path2;
3051
+ this.processedSuffix = opts?.processedSuffix ?? ".processed";
3052
+ this.supportedExtensions = opts?.supportedExtensions ?? [];
3053
+ }
3054
+ /**
3055
+ * Lists all files in the bucket with an optional prefix.
3056
+ */
3057
+ async listFiles(prefix) {
3058
+ const effectivePrefix = prefix || this.path;
3059
+ const [files] = await this.bucket.getFiles(
3060
+ effectivePrefix ? { prefix: effectivePrefix } : void 0
3061
+ );
3062
+ return files.map((f) => f.name);
3063
+ }
3064
+ /**
3065
+ * Returns files that haven't been marked as processed.
3066
+ * Filters by supported extensions if configured.
3067
+ */
3068
+ async getUnprocessedFiles() {
3069
+ const allFiles = await this.listFiles();
3070
+ const markerFiles = new Set(
3071
+ allFiles.filter((f) => f.endsWith(this.processedSuffix)).map((f) => f.replace(this.processedSuffix, ""))
3072
+ );
3073
+ return allFiles.filter((f) => {
3074
+ if (f.endsWith(this.processedSuffix)) return false;
3075
+ if (f.endsWith("/")) return false;
3076
+ if (this.supportedExtensions.length > 0) {
3077
+ const ext = pathModule.extname(f).toLowerCase();
3078
+ if (!this.supportedExtensions.includes(ext)) return false;
3079
+ }
3080
+ return !markerFiles.has(f);
3081
+ });
3082
+ }
3083
+ /**
3084
+ * Creates a marker file to indicate a file has been processed.
3085
+ */
3086
+ async createMarkerFile(fileName) {
3087
+ const markerFileName = `${fileName}${this.processedSuffix}`;
3088
+ try {
3089
+ const file = this.bucket.file(markerFileName);
3090
+ await file.save(format6("Marked file %s as processed", fileName));
3091
+ logger.info(`${logPrefix3} Marker file ${markerFileName} created successfully.`);
3092
+ } catch (err) {
3093
+ if (err instanceof Error) {
3094
+ throw new Error(format6(`error while writing marker file=%s, err:%s`, markerFileName, err.message));
3095
+ }
3096
+ throw err;
3097
+ }
3098
+ }
3099
+ /**
3100
+ * Downloads a file from GCS to a local tmp directory.
3101
+ * Returns the local path of the downloaded file.
3102
+ */
3103
+ async downloadFile(filePath, fileName) {
3104
+ const file = this.bucket.file(filePath);
3105
+ const tmpDirPath = pathModule.join(process.cwd(), tmpDir);
3106
+ if (!existsSync(tmpDirPath)) {
3107
+ mkdirSync(tmpDirPath);
3108
+ }
3109
+ const destFilename = pathModule.join(process.cwd(), tmpDir, fileName);
3110
+ try {
3111
+ if (existsSync(destFilename)) {
3112
+ unlinkSync2(destFilename);
3113
+ }
3114
+ await file.download({ destination: destFilename });
3115
+ logger.info(`${logPrefix3} Downloaded ${fileName} to ${destFilename}`);
3116
+ return destFilename;
3117
+ } catch (err) {
3118
+ if (err instanceof Error) {
3119
+ throw new Error(format6(
3120
+ `can't download file=%s from bucket, err:%s`,
3121
+ fileName,
3122
+ err.message
3123
+ ));
3124
+ }
3125
+ throw err;
3126
+ }
3127
+ }
3128
+ /**
3129
+ * Uploads a local file to GCS.
3130
+ */
3131
+ async uploadFile(localPath, destPath) {
3132
+ try {
3133
+ logger.info(`${logPrefix3} preparing to upload '%s' to '%s'`, localPath, destPath);
3134
+ const [file] = await this.bucket.upload(localPath, { destination: destPath });
3135
+ logger.info(`${logPrefix3} file %s has been successfully uploaded into %s`, localPath, destPath);
3136
+ return file;
3137
+ } catch (err) {
3138
+ if (err instanceof Error) {
3139
+ throw new Error(format6(`error while uploading file file=%s into path=%s, err:%s`, localPath, destPath, err.message));
3140
+ }
3141
+ throw err;
3142
+ }
3143
+ }
3144
+ /**
3145
+ * Reads a GCS object and returns its contents as a UTF-8 string.
3146
+ */
3147
+ async readObjectAsString(objectPath) {
3148
+ try {
3149
+ await this.bucket.get({ autoCreate: false });
3150
+ const fileRef = this.bucket.file(objectPath);
3151
+ const [exists] = await fileRef.exists();
3152
+ if (!exists) {
3153
+ throw new Error(`GCS object not found: gs://${this.bucket.name}/${objectPath}`);
3154
+ }
3155
+ const [contents] = await fileRef.download();
3156
+ return contents.toString("utf8");
3157
+ } catch (err) {
3158
+ throw new Error(format6(`error reading GCS object gs://${this.bucket.name}/${objectPath}, err: %s`, err?.message));
3159
+ }
3160
+ }
3161
+ /**
3162
+ * Writes a string to a GCS object with JSON content type.
3163
+ */
3164
+ async writeStringObject(objectPath, contents) {
3165
+ try {
3166
+ await this.bucket.get({ autoCreate: false });
3167
+ const fileRef = this.bucket.file(objectPath);
3168
+ await fileRef.save(contents, { contentType: "application/json" });
3169
+ logger.info(`Uploaded object to gs://${this.bucket.name}/${objectPath}`);
3170
+ } catch (err) {
3171
+ throw new Error(format6(`error writing GCS object gs://${this.bucket.name}/${objectPath}, err: %s`, err?.message));
3172
+ }
3173
+ }
3174
+ /**
3175
+ * Uploads a local file to the bucket with a unique name based on prefix, streamId, and UUID.
3176
+ * Returns the GCS File reference.
3177
+ */
3178
+ async uploadFileWithUniqueName(filePath, prefix, streamId) {
3179
+ try {
3180
+ await this.bucket.get({ autoCreate: false });
3181
+ const destinationFileName = `${prefix}/${streamId}-${v4_default()}.jsonnl`;
3182
+ await this.bucket.upload(filePath, {
3183
+ destination: destinationFileName
3184
+ });
3185
+ logger.info(`Uploaded ${filePath} into ${this.bucket.name}/${destinationFileName} GCS File`);
3186
+ return this.bucket.file(destinationFileName);
3187
+ } catch (err) {
3188
+ logger.error(`Issue when uploading file into GCS bucket for stream: ${streamId}
3189
+
3190
+ Error: ${err.message}
3191
+ Stack: ${err.stack}
3192
+ Code: ${err.code}
3193
+ `);
3194
+ throw err;
3195
+ }
3196
+ }
3197
+ };
3198
+
3199
+ // src/services/zip.ts
3200
+ import * as fse from "fs-extra";
3201
+ import decompress from "decompress";
3202
+ var unzip = async (zipFilePath, extractedPath) => {
3203
+ await fse.ensureDir(extractedPath);
3204
+ await decompress(zipFilePath, extractedPath);
3205
+ return extractedPath;
3206
+ };
3207
+
2138
3208
  // src/targets/bigquery/helpers.ts
2139
3209
  var safeColumnName = (key) => {
2140
3210
  let returnString = key;
@@ -2171,8 +3241,8 @@ import Decimal from "decimal.js";
2171
3241
  import dayjs5 from "dayjs";
2172
3242
  var validateDateRange = (record, schema) => {
2173
3243
  Object.keys(schema).forEach((key) => {
2174
- const { format: format6 } = schema[key];
2175
- if (format6 === "date-time") {
3244
+ const { format: format8 } = schema[key];
3245
+ if (format8 === "date-time") {
2176
3246
  if (record[key]) {
2177
3247
  const formattedDate = dayjs5(record[key]).format(defaultDateTimeFormat);
2178
3248
  const isNotTooMuchInTheFuture = dayjs5(record[key]).isBefore(dayjs5("9999-12-31 23:59:59.999999"));
@@ -2256,96 +3326,6 @@ var BqClientHolder = class {
2256
3326
  }
2257
3327
  };
2258
3328
 
2259
- // src/targets/bigquery/service/cloudStorage.ts
2260
- import { Storage } from "@google-cloud/storage";
2261
-
2262
- // node_modules/uuid/dist/esm-node/rng.js
2263
- import crypto from "crypto";
2264
- var rnds8Pool = new Uint8Array(256);
2265
- var poolPtr = rnds8Pool.length;
2266
- function rng() {
2267
- if (poolPtr > rnds8Pool.length - 16) {
2268
- crypto.randomFillSync(rnds8Pool);
2269
- poolPtr = 0;
2270
- }
2271
- return rnds8Pool.slice(poolPtr, poolPtr += 16);
2272
- }
2273
-
2274
- // node_modules/uuid/dist/esm-node/regex.js
2275
- 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;
2276
-
2277
- // node_modules/uuid/dist/esm-node/validate.js
2278
- function validate(uuid) {
2279
- return typeof uuid === "string" && regex_default.test(uuid);
2280
- }
2281
- var validate_default = validate;
2282
-
2283
- // node_modules/uuid/dist/esm-node/stringify.js
2284
- var byteToHex = [];
2285
- for (let i = 0; i < 256; ++i) {
2286
- byteToHex.push((i + 256).toString(16).substr(1));
2287
- }
2288
- function stringify3(arr, offset = 0) {
2289
- 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();
2290
- if (!validate_default(uuid)) {
2291
- throw TypeError("Stringified UUID is invalid");
2292
- }
2293
- return uuid;
2294
- }
2295
- var stringify_default = stringify3;
2296
-
2297
- // node_modules/uuid/dist/esm-node/v4.js
2298
- function v4(options, buf, offset) {
2299
- options = options || {};
2300
- const rnds = options.random || (options.rng || rng)();
2301
- rnds[6] = rnds[6] & 15 | 64;
2302
- rnds[8] = rnds[8] & 63 | 128;
2303
- if (buf) {
2304
- offset = offset || 0;
2305
- for (let i = 0; i < 16; ++i) {
2306
- buf[offset + i] = rnds[i];
2307
- }
2308
- return buf;
2309
- }
2310
- return stringify_default(rnds);
2311
- }
2312
- var v4_default = v4;
2313
-
2314
- // src/targets/bigquery/service/cloudStorage.ts
2315
- var uploadFileInBucket = async (streamId, gcsBuckerName, connectorId, filePath) => {
2316
- try {
2317
- const retryOptions = { autoRetry: true, maxRetries: 20 };
2318
- let storage = new Storage({ retryOptions });
2319
- const bucketRef = storage.bucket(gcsBuckerName);
2320
- await bucketRef.get({ autoCreate: false });
2321
- const destinationFileName = `${connectorId}/${streamId}-${v4_default()}.jsonnl`;
2322
- await bucketRef.upload(filePath, {
2323
- destination: destinationFileName
2324
- });
2325
- logger.info(`\u{1F5F3} Uploaded ${filePath} into ${gcsBuckerName}/${destinationFileName} GCS File`);
2326
- return bucketRef.file(destinationFileName);
2327
- } catch (err) {
2328
- logger.error(`Issue when uploading file into GCS bucket for stream: ${streamId}
2329
-
2330
- Error: ${err.message}
2331
- Stack: ${err.stack}
2332
- Code: ${err.code}
2333
- `);
2334
- if (err.code < 500) {
2335
- await haltAndCatchFire(
2336
- `unauthorized`,
2337
- `We couldn't connect to your Cloud Storage bucket \u{1F614}
2338
-
2339
- The error from Google is: '${err.message}' \u{1F440}
2340
-
2341
- Could you troubleshoot your Cloud Storage configuration in Google Cloud and sync again the source? \u{1F64F}`,
2342
- `Got error from cloud storage lib`
2343
- );
2344
- }
2345
- throw err;
2346
- }
2347
- };
2348
-
2349
3329
  // src/targets/bigquery/service/dbSync.ts
2350
3330
  import chunk from "lodash/chunk.js";
2351
3331
  import { Semaphore as Semaphore2 } from "async-mutex";
@@ -2386,21 +3366,21 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
2386
3366
  safeColumnName = safeColumnName;
2387
3367
  getWarehouseTypeFromJSONSchema = (definition) => {
2388
3368
  const type = definition.type;
2389
- const format6 = definition.format;
3369
+ const format8 = definition.format;
2390
3370
  let typeArr;
2391
3371
  if (type instanceof Array) {
2392
3372
  typeArr = type;
2393
3373
  } else {
2394
3374
  typeArr = [type];
2395
3375
  }
2396
- if (format6) {
2397
- switch (format6) {
3376
+ if (format8) {
3377
+ switch (format8) {
2398
3378
  case "date-time":
2399
3379
  return "TIMESTAMP";
2400
3380
  case "json":
2401
3381
  return "STRING";
2402
3382
  default:
2403
- throw new Error(`StreamId: ${this.streamId} - Unsupported format: ${format6}`);
3383
+ throw new Error(`StreamId: ${this.streamId} - Unsupported format: ${format8}`);
2404
3384
  }
2405
3385
  } else if (typeArr.includes("number")) {
2406
3386
  return "NUMERIC";
@@ -2496,6 +3476,13 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
2496
3476
  ...this.getMergeQueries()
2497
3477
  ];
2498
3478
  };
3479
+ getAppendQueries = () => {
3480
+ const columnUnsafeToSafeMapping = this.renamedColumnStore.getUnsafeToSafeColumnMapping(this.streamId);
3481
+ const columns = Object.keys(columnUnsafeToSafeMapping).map((unsafeKey) => `\`${columnUnsafeToSafeMapping[unsafeKey]}\``).join(`, `);
3482
+ return [
3483
+ `INSERT INTO ${this.sqlTableId} (${columns}) SELECT ${columns} FROM ${this.sqlStagingTableId};`
3484
+ ];
3485
+ };
2499
3486
  createDatabaseAndSchemaIfNotExists = async (retryCount) => {
2500
3487
  try {
2501
3488
  await semaphore2.runExclusive(async () => {
@@ -2620,11 +3607,11 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
2620
3607
  };
2621
3608
  loadStreamInStagingArea = async (localFilePath) => {
2622
3609
  try {
2623
- const file = await uploadFileInBucket(
2624
- this.streamId,
2625
- this.config.loading_deck_gcs_bucket_name,
3610
+ const gcsService = new CloudStorageService(this.config.loading_deck_gcs_bucket_name);
3611
+ const file = await gcsService.uploadFileWithUniqueName(
3612
+ localFilePath,
2626
3613
  this.config.connector_id,
2627
- localFilePath
3614
+ this.streamId
2628
3615
  );
2629
3616
  const metadata = {
2630
3617
  sourceFormat: "NEWLINE_DELIMITED_JSON",
@@ -2633,6 +3620,17 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
2633
3620
  await this.loadGCSFileInTable(this.sqlStagingTableId, file, metadata);
2634
3621
  } catch (err) {
2635
3622
  logger.error(`StreamId: ${this.streamId} - Error while uploading stream.`);
3623
+ if (err.code < 500) {
3624
+ await haltAndCatchFire(
3625
+ `unauthorized`,
3626
+ `We couldn't connect to your Cloud Storage bucket \u{1F614}
3627
+
3628
+ The error from Google is: '${err.message}' \u{1F440}
3629
+
3630
+ Could you troubleshoot your Cloud Storage configuration in Google Cloud and sync again the source? \u{1F64F}`,
3631
+ `Got error from cloud storage lib`
3632
+ );
3633
+ }
2636
3634
  throw err;
2637
3635
  }
2638
3636
  };
@@ -2760,47 +3758,16 @@ var BigQueryTarget = class extends ITarget {
2760
3758
  convertNumberIntoDecimal = convertNumberIntoDecimal;
2761
3759
  };
2762
3760
 
2763
- // src/sdk/service/gcs.ts
2764
- import { Storage as Storage2 } from "@google-cloud/storage";
2765
- import { format as format4 } from "util";
2766
- var getStorage = () => new Storage2({ retryOptions: { autoRetry: true, maxRetries: 20 } });
2767
- var readObjectAsString = async (bucketName, objectPath) => {
2768
- try {
2769
- const storage = getStorage();
2770
- const bucketRef = storage.bucket(bucketName);
2771
- await bucketRef.get({ autoCreate: false });
2772
- const fileRef = bucketRef.file(objectPath);
2773
- const [exists] = await fileRef.exists();
2774
- if (!exists) {
2775
- throw new Error(`GCS object not found: gs://${bucketName}/${objectPath}`);
2776
- }
2777
- const [contents] = await fileRef.download();
2778
- return contents.toString("utf8");
2779
- } catch (err) {
2780
- throw new Error(format4(`error reading GCS object gs://${bucketName}/${objectPath}, err: %s`, err?.message));
2781
- }
2782
- };
2783
- var writeStringObject = async (bucketName, objectPath, contents) => {
2784
- try {
2785
- const storage = getStorage();
2786
- const bucketRef = storage.bucket(bucketName);
2787
- await bucketRef.get({ autoCreate: false });
2788
- const fileRef = bucketRef.file(objectPath);
2789
- await fileRef.save(contents, { contentType: "application/json" });
2790
- logger.info(`\u{1F5F3} Uploaded object to gs://${bucketName}/${objectPath}`);
2791
- } catch (err) {
2792
- throw new Error(format4(`error writing GCS object gs://${bucketName}/${objectPath}, err: %s`, err?.message));
2793
- }
2794
- };
2795
-
2796
3761
  // src/state-providers/gcs/main.ts
2797
- import { format as format5 } from "util";
3762
+ import { format as format7 } from "util";
2798
3763
  var GCSStateProvider = class {
2799
3764
  bucketName;
2800
3765
  connectorId;
3766
+ service;
2801
3767
  constructor(connectorId, bucketName) {
2802
3768
  this.connectorId = connectorId;
2803
3769
  this.bucketName = bucketName;
3770
+ this.service = new CloudStorageService(bucketName);
2804
3771
  }
2805
3772
  getObjectPath() {
2806
3773
  return `${this.connectorId}/state.json`;
@@ -2809,11 +3776,11 @@ var GCSStateProvider = class {
2809
3776
  const objectPath = this.getObjectPath();
2810
3777
  try {
2811
3778
  logger.info(`\u{1F4C1} Loading state from gs://${this.bucketName}/${objectPath}`);
2812
- const raw = await readObjectAsString(this.bucketName, objectPath);
3779
+ const raw = await this.service.readObjectAsString(objectPath);
2813
3780
  const parsed = JSON.parse(raw);
2814
3781
  return { state: parsed };
2815
3782
  } catch (err) {
2816
- logger.warn(format5(`error loading state from gs://${this.bucketName}/${objectPath}, will start with empty state, err: %s`, err?.message));
3783
+ logger.warn(format7(`error loading state from gs://${this.bucketName}/${objectPath}, will start with empty state, err: %s`, err?.message));
2817
3784
  return { state: void 0 };
2818
3785
  }
2819
3786
  }
@@ -2821,10 +3788,10 @@ var GCSStateProvider = class {
2821
3788
  const objectPath = this.getObjectPath();
2822
3789
  try {
2823
3790
  logger.info(`\u{1F4DD} Writing state to gs://${this.bucketName}/${objectPath}`);
2824
- await writeStringObject(this.bucketName, objectPath, state);
3791
+ await this.service.writeStringObject(objectPath, state);
2825
3792
  } catch (err) {
2826
- logger.error(format5(`\u{1F4A5} Error while writing state to gs://${this.bucketName}/${objectPath}, err: %s`, err?.message));
2827
- throw new Error(format5(`error writing state to gs://${this.bucketName}/${objectPath}, err: %s`, err?.message));
3793
+ logger.error(format7(`\u{1F4A5} Error while writing state to gs://${this.bucketName}/${objectPath}, err: %s`, err?.message));
3794
+ throw new Error(format7(`error writing state to gs://${this.bucketName}/${objectPath}, err: %s`, err?.message));
2828
3795
  }
2829
3796
  }
2830
3797
  };
@@ -2834,9 +3801,16 @@ export {
2834
3801
  Authenticator,
2835
3802
  BATCH_INTERVAL_MS,
2836
3803
  BigQueryTarget,
3804
+ CloudStorageService,
2837
3805
  CounterMetric,
3806
+ CsvExtractionConfigBuilder,
2838
3807
  DEFAULT_MAX_CONCURRENT_STREAMS,
2839
3808
  EXECUTION_TIME_METRIC_NAME,
3809
+ ExcelExtractionConfigBuilder,
3810
+ FileFormat,
3811
+ FilePatterns,
3812
+ FileStream,
3813
+ FileTap,
2840
3814
  GCSStateProvider,
2841
3815
  ITarget,
2842
3816
  MissingFieldInSchemaError,
@@ -2849,16 +3823,33 @@ export {
2849
3823
  ReplicationMethodMessage,
2850
3824
  SchemaMessage,
2851
3825
  SchemaValidationError,
3826
+ default2 as SftpClient,
3827
+ SftpService,
2852
3828
  StateMessage,
2853
3829
  StateService,
2854
3830
  Stream,
2855
3831
  StreamWarehouseSyncService,
2856
3832
  Tap,
3833
+ VariableExtractors,
2857
3834
  _greaterThan,
2858
3835
  addWhalyFields,
3836
+ checkCsvHeaderRow,
3837
+ createCellReference,
3838
+ createCsvStreamConfig,
3839
+ createExcelGenerator,
3840
+ createExcelStreamConfig,
2859
3841
  createTemporaryFileStream,
3842
+ csvFieldsToJsonSchema,
3843
+ excelColumnToIndex,
3844
+ excelFieldsToJsonSchema,
3845
+ excelRowToIndex,
3846
+ extractExcelRows,
3847
+ extractPrimaryKeysFromCsvConfig,
3848
+ extractPrimaryKeysFromExcelFields,
2860
3849
  extractStateForStream,
3850
+ fieldTypeToJsonSchema,
2861
3851
  finalizeStateProgressMarkers,
3852
+ findAllMatchingConfigs,
2862
3853
  flattenSchema,
2863
3854
  getAllMetrics,
2864
3855
  getAxiosInstance,
@@ -2871,14 +3862,23 @@ export {
2871
3862
  gracefulExit,
2872
3863
  haltAndCatchFire,
2873
3864
  incrementStreamState,
3865
+ indexToExcelColumn,
3866
+ indexToExcelRow,
2874
3867
  loadJson,
2875
3868
  loadSchema,
2876
3869
  logger,
3870
+ parseCellReference,
2877
3871
  postFormDataApiCall,
2878
3872
  postJSONApiCall,
2879
3873
  postUrlEncodedApiCall,
2880
3874
  printMemoryFootprint,
3875
+ processFileStreams,
2881
3876
  removeParasiteProperties,
2882
- safePath
3877
+ rowGeneratorFromCsv,
3878
+ safePath,
3879
+ unzip,
3880
+ validateExcelConfig,
3881
+ writeDataToCsv,
3882
+ writeGeneratorToCSV
2883
3883
  };
2884
3884
  //# sourceMappingURL=index.mjs.map