@whaly/connector-sdk 0.2.0 → 0.3.1

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
 
@@ -727,6 +728,8 @@ var Stream = class {
727
728
  // However, a STATE will still be emitted if the Stream is using an incremental sync type.
728
729
  isSilent = false;
729
730
  childConcurrency;
731
+ // Optional total row count for percentage progress logging
732
+ totalRows;
730
733
  // Metrics configuration
731
734
  rowsSyncedMetricsConf;
732
735
  executionTimeMetricsConf;
@@ -1004,6 +1007,10 @@ var Stream = class {
1004
1007
  metric.increment();
1005
1008
  });
1006
1009
  rowsSent += 1;
1010
+ if (rowsSent % 1e3 === 0) {
1011
+ const percentStr = this.totalRows ? ` (${Math.round(rowsSent / this.totalRows * 100)}%)` : "";
1012
+ logger.info(`\u23F3 Stream: ${this.streamId} - ${rowsSent} records synced so far...${percentStr}`);
1013
+ }
1007
1014
  }
1008
1015
  const stateServiceInst = StateService.getInstance();
1009
1016
  const state = stateServiceInst.getBookmark(this.streamId);
@@ -1504,6 +1511,8 @@ var StreamWarehouseSyncService = class {
1504
1511
  mergeQueries = this.getReplaceQueries();
1505
1512
  } else if (replicationMethod === "INCREMENTAL") {
1506
1513
  mergeQueries = this.getMergeQueries();
1514
+ } else if (replicationMethod === "APPEND") {
1515
+ mergeQueries = this.getAppendQueries();
1507
1516
  } else if (replicationMethod === "LOG_BASED") {
1508
1517
  mergeQueries = this.getMergeQueries();
1509
1518
  }
@@ -1674,7 +1683,8 @@ var flattenSchema = (streamId, schema) => {
1674
1683
  };
1675
1684
 
1676
1685
  // src/sdk/models/target/target.ts
1677
- import { Validator } from "jsonschema";
1686
+ import Ajv from "ajv";
1687
+ import addFormats from "ajv-formats";
1678
1688
  import dayjs4 from "dayjs";
1679
1689
 
1680
1690
  // src/sdk/models/target/streamDbState.ts
@@ -1685,15 +1695,15 @@ import fs2 from "fs-extra";
1685
1695
  import uniqueId from "lodash/uniqueId.js";
1686
1696
  var createTemporaryFileStream = (streamId) => {
1687
1697
  fs2.ensureDirSync("./tmp");
1688
- const path = `./tmp/${safePath(streamId)}-${uniqueId()}.tmp`;
1689
- const writeStream = fs2.createWriteStream(path, { flags: "w" });
1698
+ const path2 = `./tmp/${safePath(streamId)}-${uniqueId()}.tmp`;
1699
+ const writeStream = fs2.createWriteStream(path2, { flags: "w" });
1690
1700
  return {
1691
1701
  stream: writeStream,
1692
- path
1702
+ path: path2
1693
1703
  };
1694
1704
  };
1695
- function safePath(path) {
1696
- let formattedPath = path;
1705
+ function safePath(path2) {
1706
+ let formattedPath = path2;
1697
1707
  formattedPath = formattedPath.replace(/^The\s+/gi, "");
1698
1708
  formattedPath = formattedPath.replace(/\s+(?:\(|\[)?Dis(?:c|k) (?:One|Two|Three|Four|Five|Six|Seven|Eight|Nine|Ten|\d)+(?:\)|\])?/gi, "");
1699
1709
  formattedPath = formattedPath.replace(/\s+/gi, "_");
@@ -1752,6 +1762,8 @@ var StreamState = class {
1752
1762
  // So we need to keep track of whether we are at the first load or not
1753
1763
  _hasBeenLoadedYetDuringThisSync;
1754
1764
  replicationMethod;
1765
+ // Pre-compiled ajv ValidateFunction — compiled once per schema, reused for every row
1766
+ compiledValidateFn;
1755
1767
  constructor(streamId, dbSync, replicationMethod) {
1756
1768
  this.streamId = streamId;
1757
1769
  this.dbSync = dbSync;
@@ -1769,6 +1781,12 @@ var StreamState = class {
1769
1781
  getSchema() {
1770
1782
  return this.schema;
1771
1783
  }
1784
+ setCompiledValidateFn(fn) {
1785
+ this.compiledValidateFn = fn;
1786
+ }
1787
+ getCompiledValidateFn() {
1788
+ return this.compiledValidateFn;
1789
+ }
1772
1790
  getBatchDate() {
1773
1791
  return this.batchDate.format(defaultDateTimeFormat);
1774
1792
  }
@@ -1825,8 +1843,8 @@ var ITarget = class _ITarget {
1825
1843
  flushedState;
1826
1844
  renameColumnStore;
1827
1845
  streams;
1828
- // JSON Schema validator
1829
- validator;
1846
+ // JSON Schema validator (ajv — pre-compiles schemas into optimized JS functions)
1847
+ ajv;
1830
1848
  constructor(config, stateProvider) {
1831
1849
  this.config = config;
1832
1850
  this.stateProvider = stateProvider;
@@ -1834,7 +1852,7 @@ var ITarget = class _ITarget {
1834
1852
  this.streams = {};
1835
1853
  this.batchedState = {};
1836
1854
  this.flushedState = {};
1837
- this.validator = new Validator();
1855
+ this.ajv = addFormats(new Ajv({ allErrors: true, strict: false }));
1838
1856
  this.syncTime = dayjs4();
1839
1857
  this.renameColumnStore = new RenameColumnStore();
1840
1858
  }
@@ -1893,12 +1911,14 @@ var ITarget = class _ITarget {
1893
1911
  if (!stream) {
1894
1912
  throw new MissingSchemaError(`A record for stream:\`${streamId}\` was encountered before a corresponding schema.`, streamId);
1895
1913
  }
1896
- await semaphore.runExclusive(async () => {
1897
- const shouldUploadIntermediateBatch = this.shouldUploadIntermediateBatch(streamId);
1898
- if (shouldUploadIntermediateBatch) {
1899
- await this.loadAllStreamsInWarehouse({ isFinalLoad: false });
1900
- }
1901
- });
1914
+ if (stream.getBatchedRowCount() % 1e4 === 0) {
1915
+ await semaphore.runExclusive(async () => {
1916
+ const shouldUploadIntermediateBatch = this.shouldUploadIntermediateBatch(streamId);
1917
+ if (shouldUploadIntermediateBatch) {
1918
+ await this.loadAllStreamsInWarehouse({ isFinalLoad: false });
1919
+ }
1920
+ });
1921
+ }
1902
1922
  const record = message.record;
1903
1923
  const schema = stream.getSchema();
1904
1924
  if (!schema) {
@@ -1907,13 +1927,13 @@ var ITarget = class _ITarget {
1907
1927
  }
1908
1928
  const recordWithoutParasiteProperties = removeParasiteProperties(record, schema);
1909
1929
  const recordWithWhalyFields = addWhalyFields(recordWithoutParasiteProperties, stream.getBatchDate());
1910
- const validationResult = this.validator.validate(recordWithWhalyFields, schema);
1911
- if (!validationResult.valid) {
1930
+ const validateFn = stream.getCompiledValidateFn();
1931
+ if (validateFn && !validateFn(recordWithWhalyFields)) {
1912
1932
  throw new SchemaValidationError(`Stream: ${streamId} - Record is not valid according to schema.
1913
- Validation errors: ${JSON.stringify(validationResult.errors)}
1933
+ Validation errors: ${JSON.stringify(validateFn.errors)}
1914
1934
 
1915
1935
  Record: ${JSON.stringify(recordWithoutParasiteProperties)}
1916
- Schema: ${JSON.stringify(stream.getSchema())}`, streamId, validationResult.errors);
1936
+ Schema: ${JSON.stringify(stream.getSchema())}`, streamId, validateFn.errors ?? []);
1917
1937
  }
1918
1938
  const recordWithValidDate = this.validateDateRange(recordWithWhalyFields, schema);
1919
1939
  const recordWithDecimal = this.convertNumberIntoDecimal(recordWithValidDate, schema);
@@ -2002,7 +2022,9 @@ var ITarget = class _ITarget {
2002
2022
  }
2003
2023
  });
2004
2024
  streamState.setSchema(schema);
2005
- if (message.keyProperties.length === 0) {
2025
+ streamState.setCompiledValidateFn(this.ajv.compile({ type: "object", properties: schema }));
2026
+ const streamReplicationMethod = this.streams[streamId]?.getReplicationMethod();
2027
+ if (message.keyProperties.length === 0 && streamReplicationMethod !== "APPEND" /* APPEND */) {
2006
2028
  throw new Error(`StreamId: ${message.stream} - \`key_properties\` field is required and can't be empty in SCHEMA message.
2007
2029
  Received SCHEMA message: ${JSON.stringify(message)}`);
2008
2030
  }
@@ -2066,7 +2088,8 @@ var ITarget = class _ITarget {
2066
2088
  return false;
2067
2089
  }
2068
2090
  const batchedRecords = stream.getBatchedRowCount();
2069
- if (stream.getReplicationMethod() === "INCREMENTAL" && batchedRecords > 1e6) {
2091
+ const method = stream.getReplicationMethod();
2092
+ if ((method === "INCREMENTAL" || method === "APPEND") && batchedRecords > 1e6) {
2070
2093
  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
2094
  return true;
2072
2095
  } else {
@@ -2135,6 +2158,1037 @@ var ITarget = class _ITarget {
2135
2158
  };
2136
2159
  };
2137
2160
 
2161
+ // src/sdk/file-processing/types.ts
2162
+ var FileFormat = /* @__PURE__ */ ((FileFormat2) => {
2163
+ FileFormat2["CSV"] = "CSV";
2164
+ FileFormat2["EXCEL"] = "EXCEL";
2165
+ return FileFormat2;
2166
+ })(FileFormat || {});
2167
+
2168
+ // src/sdk/file-processing/schema-utils.ts
2169
+ function fieldTypeToJsonSchema(fieldType) {
2170
+ switch (fieldType) {
2171
+ case "STRING":
2172
+ return { type: ["null", "string"] };
2173
+ case "FLOAT":
2174
+ return { type: ["null", "number"] };
2175
+ case "TIMESTAMP":
2176
+ return { type: ["null", "string"], format: "date-time" };
2177
+ default:
2178
+ return { type: ["null", "string"] };
2179
+ }
2180
+ }
2181
+ function excelFieldsToJsonSchema(fields) {
2182
+ const properties = {};
2183
+ for (const [key, field] of Object.entries(fields)) {
2184
+ properties[key] = fieldTypeToJsonSchema(field.type);
2185
+ }
2186
+ return { type: "object", properties };
2187
+ }
2188
+ function csvFieldsToJsonSchema(config) {
2189
+ const properties = {};
2190
+ if (Array.isArray(config)) {
2191
+ for (const field of config) {
2192
+ properties[field.key] = fieldTypeToJsonSchema(field.type);
2193
+ }
2194
+ } else {
2195
+ for (const [, field] of Object.entries(config)) {
2196
+ properties[field.key] = fieldTypeToJsonSchema(field.type);
2197
+ }
2198
+ }
2199
+ return { type: "object", properties };
2200
+ }
2201
+ function extractPrimaryKeysFromCsvConfig(config) {
2202
+ if (Array.isArray(config)) {
2203
+ return config.map((f) => f.key);
2204
+ }
2205
+ return Object.values(config).map((f) => f.key);
2206
+ }
2207
+ function extractPrimaryKeysFromExcelFields(fields) {
2208
+ return Object.entries(fields).filter(([, field]) => {
2209
+ return field.primaryKey === true || field.primaryKey === true;
2210
+ }).map(([key]) => key);
2211
+ }
2212
+
2213
+ // src/sdk/file-processing/excel/reader.ts
2214
+ import XLSX from "xlsx";
2215
+ import path from "path";
2216
+ var excelColumnToIndex = (columnLetter) => {
2217
+ const letters = columnLetter.toUpperCase();
2218
+ let index = 0;
2219
+ for (let i = 0; i < letters.length; i++) {
2220
+ const charCode = letters.charCodeAt(i);
2221
+ if (charCode < 65 || charCode > 90) {
2222
+ throw new Error(`Invalid column letter: ${columnLetter}`);
2223
+ }
2224
+ index = index * 26 + (charCode - 64);
2225
+ }
2226
+ return index - 1;
2227
+ };
2228
+ var indexToExcelColumn = (columnIndex) => {
2229
+ if (columnIndex < 0) {
2230
+ throw new Error(`Invalid column index: ${columnIndex}. Must be >= 0`);
2231
+ }
2232
+ let result = "";
2233
+ let index = columnIndex;
2234
+ while (true) {
2235
+ result = String.fromCharCode(65 + index % 26) + result;
2236
+ index = Math.floor(index / 26);
2237
+ if (index === 0) break;
2238
+ index--;
2239
+ }
2240
+ return result;
2241
+ };
2242
+ var excelRowToIndex = (rowNumber) => {
2243
+ const rowNum = typeof rowNumber === "string" ? parseInt(rowNumber, 10) : rowNumber;
2244
+ if (isNaN(rowNum) || rowNum < 1) {
2245
+ throw new Error(`Invalid row number: ${rowNumber}. Must be >= 1`);
2246
+ }
2247
+ return rowNum - 1;
2248
+ };
2249
+ var indexToExcelRow = (index) => {
2250
+ if (index < 0) {
2251
+ throw new Error(`Invalid index: ${index}. Must be >= 0`);
2252
+ }
2253
+ return index + 1;
2254
+ };
2255
+ var parseCellReference = (cellRef) => {
2256
+ const match = cellRef.match(/^([A-Z]+)(\d+)$/);
2257
+ if (!match) {
2258
+ throw new Error(`Invalid cell reference: ${cellRef}. Expected format like "A1", "Z24", "AA100"`);
2259
+ }
2260
+ const [, columnLetter, rowNumber] = match;
2261
+ return {
2262
+ columnIndex: excelColumnToIndex(columnLetter),
2263
+ rowIndex: excelRowToIndex(rowNumber)
2264
+ };
2265
+ };
2266
+ var createCellReference = (columnIndex, rowIndex) => {
2267
+ return indexToExcelColumn(columnIndex) + indexToExcelRow(rowIndex);
2268
+ };
2269
+ var extractSingleSheetRows = async (fileName, conf) => {
2270
+ console.log("Extracting single sheet rows from file: %s", fileName);
2271
+ const workbook = XLSX.readFile(fileName, { dense: true });
2272
+ const sheetKeys = Object.keys(workbook.Sheets);
2273
+ const sheet = conf.sheetName ? workbook.Sheets[conf.sheetName] : workbook.Sheets[sheetKeys[0]];
2274
+ if (!sheet) {
2275
+ throw new Error(`Sheet ${conf.sheetName} not found in workbook`);
2276
+ }
2277
+ const sheetData = sheet["!data"];
2278
+ if (!sheetData) {
2279
+ throw new Error(`No data in ${conf.sheetName || "first"} sheet`);
2280
+ }
2281
+ if (sheetData.length === 0) {
2282
+ throw new Error(`Sheet ${conf.sheetName || "first"} is empty`);
2283
+ }
2284
+ const variables = conf.fileNameVariablesExtractor(fileName, workbook);
2285
+ const data = [];
2286
+ let rowIdx = 0;
2287
+ const minColumnIndex = Object.values(conf.columns).reduce((min, column) => {
2288
+ return column.column ? Math.min(min, excelColumnToIndex(column.column)) : min;
2289
+ }, Number.MAX_SAFE_INTEGER);
2290
+ for (const row of sheetData) {
2291
+ if (rowIdx < conf.numberOfRowsToSkip) {
2292
+ rowIdx++;
2293
+ continue;
2294
+ }
2295
+ if (!row) {
2296
+ console.log("Empty row at index: %s, skipped processing.", rowIdx);
2297
+ continue;
2298
+ }
2299
+ if (minColumnIndex !== Number.MAX_SAFE_INTEGER && !row[minColumnIndex]?.v) {
2300
+ console.log("stopping processing because of empty value in the first data column (idx: %s)", minColumnIndex);
2301
+ break;
2302
+ }
2303
+ const rowData = Object.entries(conf.columns).reduce((acc, [key, column]) => {
2304
+ if (column.variableName) {
2305
+ if (!variables) {
2306
+ throw new Error(`No variables extracted from filename, cannot extract derived field ${key}`);
2307
+ }
2308
+ acc[key] = variables[column.variableName] || "";
2309
+ } else {
2310
+ const colIndex = excelColumnToIndex(column.column);
2311
+ acc[key] = row[colIndex]?.v || "";
2312
+ }
2313
+ return acc;
2314
+ }, {});
2315
+ data.push(rowData);
2316
+ rowIdx++;
2317
+ }
2318
+ console.log("Extracted %s rows from sheet %s", data.length, conf.sheetName || "first");
2319
+ return data;
2320
+ };
2321
+ var extractExcelRows = async (localFilePath, conf) => {
2322
+ let data;
2323
+ if (conf.type === "single-sheet-extraction") {
2324
+ data = await extractSingleSheetRows(localFilePath, conf);
2325
+ } else if (conf.type === "processor") {
2326
+ const workbook = XLSX.readFile(localFilePath, { dense: true });
2327
+ data = await conf.processor(workbook);
2328
+ } else {
2329
+ throw new Error(`Unsupported configuration type: ${conf.type}`);
2330
+ }
2331
+ return data;
2332
+ };
2333
+ var validateExcelConfig = (config) => {
2334
+ if (!config.extension) {
2335
+ throw new Error("Extension is required");
2336
+ }
2337
+ if (!config.tableName) {
2338
+ throw new Error("Table name is required");
2339
+ }
2340
+ if (!config.fileNameValidator || typeof config.fileNameValidator !== "function") {
2341
+ throw new Error("File name validator function is required");
2342
+ }
2343
+ if (!config.fileNameVariablesExtractor || typeof config.fileNameVariablesExtractor !== "function") {
2344
+ throw new Error("File name variables extractor function is required");
2345
+ }
2346
+ if (config.type === "single-sheet-extraction" && (!config.columns || Object.keys(config.columns).length === 0)) {
2347
+ throw new Error("At least one column configuration is required for single-sheet-extraction");
2348
+ }
2349
+ if (config.type === "single-sheet-extraction") {
2350
+ Object.entries(config.columns).forEach(([key, column]) => {
2351
+ if (!column.type) {
2352
+ throw new Error(`Column ${key} must have a type`);
2353
+ }
2354
+ if (column.column) {
2355
+ const colLetter = column.column;
2356
+ if (!/^[A-Z]+$/i.test(colLetter)) {
2357
+ throw new Error(`Invalid column letter format: ${colLetter}`);
2358
+ }
2359
+ } else if (column.variableName) {
2360
+ } else {
2361
+ throw new Error(`Column ${key} must specify either 'column' or 'variableName'`);
2362
+ }
2363
+ });
2364
+ if (config.numberOfRowsToSkip === void 0 || config.numberOfRowsToSkip < 0) {
2365
+ throw new Error("Number of rows to skip must be specified and non-negative");
2366
+ }
2367
+ } else if (config.type === "processor") {
2368
+ if (!config.processor || typeof config.processor !== "function") {
2369
+ throw new Error("Processor function is required for processor type");
2370
+ }
2371
+ }
2372
+ };
2373
+ var findAllMatchingConfigs = (filename, configs) => {
2374
+ const fileInfo = path.parse(filename);
2375
+ return configs.filter(
2376
+ (config) => config.fileNameValidator(fileInfo.base) && config.extension === fileInfo.ext.replace(".", "")
2377
+ );
2378
+ };
2379
+ async function* createExcelGenerator(data) {
2380
+ for (const row of data) {
2381
+ yield row;
2382
+ }
2383
+ }
2384
+
2385
+ // src/sdk/file-processing/excel/config-builder.ts
2386
+ var ExcelExtractionConfigBuilder = class _ExcelExtractionConfigBuilder {
2387
+ config = {
2388
+ fileNameValidator: () => true,
2389
+ fileNameVariablesExtractor: () => ({}),
2390
+ replicationMethod: "FULL_TABLE" /* FULL_TABLE */
2391
+ };
2392
+ static create() {
2393
+ return new _ExcelExtractionConfigBuilder();
2394
+ }
2395
+ extension(ext) {
2396
+ this.config.extension = ext;
2397
+ return this;
2398
+ }
2399
+ tableName(name) {
2400
+ this.config.tableName = name;
2401
+ return this;
2402
+ }
2403
+ fileValidator(validator) {
2404
+ this.config.fileNameValidator = validator;
2405
+ return this;
2406
+ }
2407
+ variablesExtractor(extractor) {
2408
+ this.config.fileNameVariablesExtractor = extractor;
2409
+ return this;
2410
+ }
2411
+ replicationMethod(method) {
2412
+ this.config.replicationMethod = method;
2413
+ return this;
2414
+ }
2415
+ columns(cols) {
2416
+ if (this.config.type === "single-sheet-extraction") {
2417
+ this.config.columns = cols;
2418
+ } else {
2419
+ throw new Error("Columns can only be set for single-sheet-extraction");
2420
+ }
2421
+ return this;
2422
+ }
2423
+ singleSheet(sheetName, skipRows = 0) {
2424
+ this.config.type = "single-sheet-extraction";
2425
+ if (sheetName !== void 0) {
2426
+ this.config.sheetName = sheetName;
2427
+ }
2428
+ this.config.numberOfRowsToSkip = skipRows;
2429
+ return this;
2430
+ }
2431
+ processor(processorFn) {
2432
+ this.config.type = "processor";
2433
+ this.config.processor = processorFn;
2434
+ return this;
2435
+ }
2436
+ build() {
2437
+ if (!this.config.type) {
2438
+ throw new Error("Configuration type must be specified (singleSheet or processor)");
2439
+ }
2440
+ if (!this.config.extension) {
2441
+ throw new Error("Extension must be set");
2442
+ }
2443
+ if (!this.config.tableName) {
2444
+ throw new Error("Table name must be set");
2445
+ }
2446
+ if (!this.config.replicationMethod) {
2447
+ throw new Error("Replication method must be set");
2448
+ }
2449
+ if (typeof this.config.fileNameValidator !== "function") {
2450
+ throw new Error("fileNameValidator must be a function");
2451
+ }
2452
+ if (typeof this.config.fileNameVariablesExtractor !== "function") {
2453
+ throw new Error("fileNameVariablesExtractor must be a function");
2454
+ }
2455
+ if (this.config.type === "single-sheet-extraction") {
2456
+ const singleSheetConfig = this.config;
2457
+ if (!singleSheetConfig.columns || Object.keys(singleSheetConfig.columns).length === 0) {
2458
+ throw new Error("Columns must be set with at least one column for single-sheet-extraction");
2459
+ }
2460
+ }
2461
+ if (this.config.type === "processor") {
2462
+ const processorConfig = this.config;
2463
+ if (typeof processorConfig.processor !== "function") {
2464
+ throw new Error("Processor function must be set for processor configs");
2465
+ }
2466
+ }
2467
+ return this.config;
2468
+ }
2469
+ };
2470
+
2471
+ // src/sdk/file-processing/csv/reader.ts
2472
+ import csv from "csv-parser";
2473
+ import { createReadStream } from "fs";
2474
+ import { format as format4 } from "util";
2475
+ import { Readable, Transform } from "stream";
2476
+ import * as iconv from "iconv-lite";
2477
+ var logPrefix = "[csvReader]";
2478
+ var createStripBomTransform = () => {
2479
+ let isFirstChunk = true;
2480
+ return new Transform({
2481
+ transform(chunk2, encoding, callback) {
2482
+ if (isFirstChunk) {
2483
+ isFirstChunk = false;
2484
+ if (chunk2.length >= 3 && chunk2[0] === 239 && chunk2[1] === 187 && chunk2[2] === 191) {
2485
+ chunk2 = chunk2.slice(3);
2486
+ }
2487
+ }
2488
+ callback(null, chunk2);
2489
+ }
2490
+ });
2491
+ };
2492
+ var getStringHexInfo = (str) => {
2493
+ const bytes = Buffer.from(str, "utf8");
2494
+ const hex = bytes.toString("hex").match(/.{2}/g)?.join(" ") || "";
2495
+ return {
2496
+ original: str,
2497
+ length: str.length,
2498
+ hex,
2499
+ trimmed: str.trim()
2500
+ };
2501
+ };
2502
+ var formatHexComparison = (expected, actual) => {
2503
+ const expectedInfo = getStringHexInfo(expected);
2504
+ const actualInfo = getStringHexInfo(actual);
2505
+ return `
2506
+ Expected: "${expected}" (${expectedInfo.length} chars) [${expectedInfo.hex}]
2507
+ Actual: "${actual}" (${actualInfo.length} chars) [${actualInfo.hex}]`;
2508
+ };
2509
+ async function* rowGeneratorFromCsv(path2, fileConfig) {
2510
+ const isGeneratorConfigArray = Array.isArray(fileConfig.fields);
2511
+ const configKeys = isGeneratorConfigArray ? [] : Object.keys(fileConfig.fields);
2512
+ const csvOptions = { separator: fileConfig.separator };
2513
+ if (isGeneratorConfigArray) {
2514
+ csvOptions.headers = false;
2515
+ }
2516
+ const csvStream = csv(csvOptions);
2517
+ const syncedAt = (/* @__PURE__ */ new Date()).toISOString();
2518
+ let initialReadStream;
2519
+ let finalInputStream;
2520
+ const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
2521
+ if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
2522
+ initialReadStream = createReadStream(path2);
2523
+ finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
2524
+ } else {
2525
+ initialReadStream = createReadStream(path2);
2526
+ finalInputStream = initialReadStream.pipe(createStripBomTransform());
2527
+ }
2528
+ finalInputStream.pipe(csvStream);
2529
+ try {
2530
+ for await (const row of Readable.from(csvStream)) {
2531
+ if (!isGeneratorConfigArray) {
2532
+ const rowData = fileConfig.addSyncedAtColumn ? { _wly_synced_at: syncedAt } : {};
2533
+ for (const key of configKeys) {
2534
+ const keyGenerator = fileConfig.fields[key.trim()];
2535
+ if (!keyGenerator) continue;
2536
+ rowData[keyGenerator.key] = keyGenerator.valueTransformer ? keyGenerator.valueTransformer(row[key]) : row[key];
2537
+ }
2538
+ yield rowData;
2539
+ } else {
2540
+ const rowKeys = Object.keys(row);
2541
+ const rowData = fileConfig.addSyncedAtColumn ? { _wly_synced_at: syncedAt } : {};
2542
+ for (let i = 0; i < rowKeys.length; i++) {
2543
+ const keyGenerator = fileConfig.fields[i];
2544
+ if (!keyGenerator) continue;
2545
+ rowData[keyGenerator.key] = keyGenerator.valueTransformer ? keyGenerator.valueTransformer(row[i]) : row[i];
2546
+ }
2547
+ yield rowData;
2548
+ }
2549
+ }
2550
+ } finally {
2551
+ initialReadStream.destroy();
2552
+ }
2553
+ }
2554
+ async function countCsvLines(filePath) {
2555
+ return new Promise((resolve, reject) => {
2556
+ let newlines = 0;
2557
+ createReadStream(filePath).on("data", (chunk2) => {
2558
+ const buf = Buffer.isBuffer(chunk2) ? chunk2 : Buffer.from(chunk2);
2559
+ for (let i = 0; i < buf.length; i++) {
2560
+ if (buf[i] === 10) newlines++;
2561
+ }
2562
+ }).on("end", () => resolve(Math.max(0, newlines - 1))).on("error", reject);
2563
+ });
2564
+ }
2565
+ var checkCsvHeaderRow = async (path2, fileConfig) => {
2566
+ return new Promise((resolve, reject) => {
2567
+ const colsWithParsingConfig = Object.keys(fileConfig.fields).map((key) => key.trim());
2568
+ console.debug(`${logPrefix} CSV Expected columns:`, colsWithParsingConfig.map((col) => {
2569
+ const info = getStringHexInfo(col);
2570
+ return `"${info.original}" [${info.hex}]`;
2571
+ }));
2572
+ const isGeneratorConfigArray = Array.isArray(fileConfig.fields);
2573
+ const csvOptions2 = { separator: fileConfig.separator };
2574
+ if (isGeneratorConfigArray) {
2575
+ csvOptions2.headers = false;
2576
+ }
2577
+ const csvStream = csv(csvOptions2);
2578
+ let initialReadStream;
2579
+ let finalInputStream;
2580
+ const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
2581
+ if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
2582
+ initialReadStream = createReadStream(path2);
2583
+ finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
2584
+ } else {
2585
+ initialReadStream = createReadStream(path2);
2586
+ finalInputStream = initialReadStream.pipe(createStripBomTransform());
2587
+ }
2588
+ let isFirstLine = true;
2589
+ finalInputStream.pipe(csvStream).on("data", (row) => {
2590
+ if (isFirstLine) {
2591
+ let headers = [];
2592
+ if (isGeneratorConfigArray) {
2593
+ headers = Object.values(row).filter((r) => !!r);
2594
+ } else {
2595
+ headers = Object.keys(row).filter((r) => !!r);
2596
+ }
2597
+ console.debug(`${logPrefix} CSV Detected headers:`, headers.map((h, idx) => {
2598
+ const info = getStringHexInfo(h);
2599
+ return `[${idx}]: "${info.original}" [${info.hex}]`;
2600
+ }));
2601
+ if (isGeneratorConfigArray) {
2602
+ colsWithParsingConfig.forEach((col, i) => {
2603
+ if (col !== headers[i]) {
2604
+ const hexComparison = formatHexComparison(col, headers[i] || "<undefined>");
2605
+ console.error(`${logPrefix} CSV Header Mismatch:${hexComparison}`);
2606
+ throw new Error(format4(
2607
+ `CSV header mismatch at position %s: expected '%s' but got '%s' in file %s`,
2608
+ i,
2609
+ col,
2610
+ headers[i] || "<undefined>",
2611
+ path2
2612
+ ));
2613
+ }
2614
+ });
2615
+ } else {
2616
+ colsWithParsingConfig.forEach((col) => {
2617
+ if (!headers.includes(col)) {
2618
+ const colInfo = getStringHexInfo(col);
2619
+ console.error(`CSV Missing column: "${col}" [${colInfo.hex}]`);
2620
+ console.error(`Available headers:`, headers.map((h) => `"${h}" [${getStringHexInfo(h).hex}]`));
2621
+ const trimmedMatch = headers.find((h) => h.trim() === col.trim());
2622
+ if (trimmedMatch) {
2623
+ console.error(`Potential trimmed match: "${trimmedMatch}" [${getStringHexInfo(trimmedMatch).hex}]`);
2624
+ }
2625
+ throw new Error(format4(
2626
+ `CSV missing expected column '%s' in file: %s`,
2627
+ col,
2628
+ path2
2629
+ ));
2630
+ }
2631
+ });
2632
+ }
2633
+ isFirstLine = false;
2634
+ } else {
2635
+ initialReadStream.destroy();
2636
+ }
2637
+ }).on("error", (error) => {
2638
+ reject(new Error(`Error processing CSV file: ${error.message}`));
2639
+ });
2640
+ initialReadStream.on("close", () => {
2641
+ resolve();
2642
+ });
2643
+ });
2644
+ };
2645
+
2646
+ // src/sdk/file-processing/csv/writer.ts
2647
+ import { createObjectCsvWriter } from "csv-writer";
2648
+ import { format as format5 } from "util";
2649
+ var logPrefix2 = "[csvWriter]";
2650
+ var writeDataToCsv = async (fileName, data) => {
2651
+ try {
2652
+ const firstRow = data[0];
2653
+ if (!firstRow) {
2654
+ console.log(`${logPrefix2} No data to write to CSV file=%s`, fileName);
2655
+ return;
2656
+ }
2657
+ const csvWriter = createObjectCsvWriter({
2658
+ path: fileName,
2659
+ header: Object.keys(firstRow).map((col) => {
2660
+ return { id: col, title: col };
2661
+ })
2662
+ });
2663
+ await csvWriter.writeRecords(data);
2664
+ console.log(`${logPrefix2} CSV file=%s written successfully`, fileName);
2665
+ } catch (err) {
2666
+ if (err instanceof Error) {
2667
+ throw new Error(format5(`error while writing csv file=%s, err: %s`, fileName, err.message));
2668
+ }
2669
+ throw err;
2670
+ }
2671
+ };
2672
+ var writeGeneratorToCSV = async (generator, outputFileName) => {
2673
+ try {
2674
+ let rowCount = 0;
2675
+ const firstDataRow = (await generator.next()).value;
2676
+ if (!firstDataRow) {
2677
+ console.log(`${logPrefix2} [%s] first data row of csv empty, skipping`, outputFileName);
2678
+ return 0;
2679
+ }
2680
+ console.log(`${logPrefix2} [%s] inferring headers from first data row`, outputFileName);
2681
+ const headers = Object.keys(firstDataRow).map((col) => {
2682
+ return {
2683
+ id: col,
2684
+ title: col
2685
+ };
2686
+ });
2687
+ const csvWriter = createObjectCsvWriter({
2688
+ path: outputFileName,
2689
+ header: headers,
2690
+ alwaysQuote: true
2691
+ });
2692
+ console.log(`${logPrefix2} [%s] writing csv`, outputFileName);
2693
+ let batch = [firstDataRow];
2694
+ for await (const data of generator) {
2695
+ batch.push(data);
2696
+ rowCount++;
2697
+ if (batch.length > 1e4) {
2698
+ try {
2699
+ await csvWriter.writeRecords(batch);
2700
+ } catch (err) {
2701
+ if (err instanceof Error) {
2702
+ throw new Error(
2703
+ format5(
2704
+ `error while writing batch to csv batch (first 3 records)=%j, err: %s`,
2705
+ batch.slice(0, 3),
2706
+ err.message
2707
+ )
2708
+ );
2709
+ }
2710
+ throw err;
2711
+ }
2712
+ batch = [];
2713
+ }
2714
+ }
2715
+ await csvWriter.writeRecords(batch);
2716
+ console.log(`${logPrefix2} [%s] csv written successfully with %d rows`, outputFileName, rowCount);
2717
+ return rowCount;
2718
+ } catch (err) {
2719
+ if (err instanceof Error) {
2720
+ throw new Error(format5(`error while writing csv file=%s, err: %s`, outputFileName, err.message));
2721
+ }
2722
+ throw err;
2723
+ }
2724
+ };
2725
+
2726
+ // src/sdk/file-processing/file-stream.ts
2727
+ import XLSX2 from "xlsx";
2728
+ var FileStream = class extends Stream {
2729
+ localFilePaths;
2730
+ constructor(config, localFilePath, tapState, target) {
2731
+ super(config, tapState, target);
2732
+ this.localFilePaths = Array.isArray(localFilePath) ? localFilePath : [localFilePath];
2733
+ this.streamId = config.streamId;
2734
+ this.primaryKey = config.primaryKeys;
2735
+ this.replicationMethod = config.replicationMethod;
2736
+ }
2737
+ /**
2738
+ * Build JSON Schema from the file config's field definitions.
2739
+ */
2740
+ async getSchema() {
2741
+ if (this.config.format === "EXCEL" /* EXCEL */) {
2742
+ const excelConfig = this.config.excel;
2743
+ if (excelConfig.type === "single-sheet-extraction") {
2744
+ const jsonSchema = excelFieldsToJsonSchema(excelConfig.columns);
2745
+ return { jsonSchema };
2746
+ }
2747
+ return void 0;
2748
+ }
2749
+ if (this.config.format === "CSV" /* CSV */) {
2750
+ const jsonSchema = csvFieldsToJsonSchema(this.config.csv.fields);
2751
+ return { jsonSchema };
2752
+ }
2753
+ throw new Error(`FileStream: No valid format config for stream ${this.streamId}`);
2754
+ }
2755
+ /**
2756
+ * Yield records from all file(s).
2757
+ * When multiple file paths are provided, records are yielded sequentially from each file.
2758
+ * Pre-counts total rows across all files to enable percentage progress logging.
2759
+ */
2760
+ async *_getRecords() {
2761
+ let total = 0;
2762
+ for (const filePath of this.localFilePaths) {
2763
+ if (this.config.format === "EXCEL" /* EXCEL */) {
2764
+ const data = await extractExcelRows(filePath, this.config.excel);
2765
+ total += data.length;
2766
+ } else if (this.config.format === "CSV" /* CSV */) {
2767
+ total += await countCsvLines(filePath);
2768
+ }
2769
+ }
2770
+ this.totalRows = total;
2771
+ for (const filePath of this.localFilePaths) {
2772
+ if (this.config.format === "EXCEL" /* EXCEL */) {
2773
+ const data = await extractExcelRows(filePath, this.config.excel);
2774
+ for (const row of data) {
2775
+ yield row;
2776
+ }
2777
+ } else if (this.config.format === "CSV" /* CSV */) {
2778
+ yield* rowGeneratorFromCsv(filePath, this.config.csv);
2779
+ } else {
2780
+ throw new Error(`FileStream: Unsupported format for stream ${this.streamId}`);
2781
+ }
2782
+ }
2783
+ }
2784
+ };
2785
+ function createExcelStreamConfig(excelConfig, fileName, localFilePath) {
2786
+ if (excelConfig.type === "single-sheet-extraction") {
2787
+ const primaryKeys = extractPrimaryKeysFromExcelFields(excelConfig.columns);
2788
+ let tableName2;
2789
+ if (typeof excelConfig.tableName === "function") {
2790
+ const variables = excelConfig.fileNameVariablesExtractor(fileName);
2791
+ tableName2 = excelConfig.tableName(fileName, void 0, variables);
2792
+ } else {
2793
+ tableName2 = excelConfig.tableName;
2794
+ }
2795
+ return {
2796
+ format: "EXCEL" /* EXCEL */,
2797
+ streamId: tableName2,
2798
+ replicationMethod: excelConfig.replicationMethod,
2799
+ primaryKeys,
2800
+ excel: excelConfig
2801
+ };
2802
+ }
2803
+ let tableName;
2804
+ if (typeof excelConfig.tableName === "function") {
2805
+ if (!localFilePath) {
2806
+ throw new Error("localFilePath is required for processor configs with dynamic table names");
2807
+ }
2808
+ const workbook = XLSX2.readFile(localFilePath, { dense: true });
2809
+ const variables = excelConfig.fileNameVariablesExtractor(fileName, workbook);
2810
+ tableName = excelConfig.tableName(fileName, workbook, variables);
2811
+ } else {
2812
+ tableName = excelConfig.tableName;
2813
+ }
2814
+ return {
2815
+ format: "EXCEL" /* EXCEL */,
2816
+ streamId: tableName,
2817
+ replicationMethod: excelConfig.replicationMethod,
2818
+ primaryKeys: [],
2819
+ excel: excelConfig
2820
+ };
2821
+ }
2822
+ function createCsvStreamConfig(streamId, csvConfig, options) {
2823
+ return {
2824
+ format: "CSV" /* CSV */,
2825
+ streamId,
2826
+ replicationMethod: options?.replicationMethod ?? "FULL_TABLE" /* FULL_TABLE */,
2827
+ primaryKeys: options?.primaryKeys ?? extractPrimaryKeysFromCsvConfig(csvConfig.fields),
2828
+ csv: csvConfig
2829
+ };
2830
+ }
2831
+ async function processFileStreams(entries, tapState, target) {
2832
+ for (const entry of entries) {
2833
+ const stream = new FileStream(entry.config, entry.filePath, tapState, target);
2834
+ await stream.sync();
2835
+ }
2836
+ await target.complete();
2837
+ }
2838
+
2839
+ // src/sdk/file-processing/csv/config-builder.ts
2840
+ var CsvExtractionConfigBuilder = class _CsvExtractionConfigBuilder {
2841
+ _separator = ",";
2842
+ _encoding;
2843
+ _addSyncedAtColumn;
2844
+ _fields;
2845
+ _fieldsMode;
2846
+ _replicationMethod;
2847
+ _primaryKeys;
2848
+ static create() {
2849
+ return new _CsvExtractionConfigBuilder();
2850
+ }
2851
+ separator(sep) {
2852
+ this._separator = sep;
2853
+ return this;
2854
+ }
2855
+ encoding(enc) {
2856
+ this._encoding = enc;
2857
+ return this;
2858
+ }
2859
+ addSyncedAtColumn(enabled = true) {
2860
+ this._addSyncedAtColumn = enabled;
2861
+ return this;
2862
+ }
2863
+ fieldsFromDict(mapping) {
2864
+ if (this._fieldsMode === "array") {
2865
+ throw new Error("Cannot use fieldsFromDict after fieldsFromArray");
2866
+ }
2867
+ this._fieldsMode = "dict";
2868
+ this._fields = mapping;
2869
+ return this;
2870
+ }
2871
+ fieldsFromArray(fields) {
2872
+ if (this._fieldsMode === "dict") {
2873
+ throw new Error("Cannot use fieldsFromArray after fieldsFromDict");
2874
+ }
2875
+ this._fieldsMode = "array";
2876
+ this._fields = fields;
2877
+ return this;
2878
+ }
2879
+ replicationMethod(method) {
2880
+ this._replicationMethod = method;
2881
+ return this;
2882
+ }
2883
+ primaryKeys(keys) {
2884
+ this._primaryKeys = keys;
2885
+ return this;
2886
+ }
2887
+ build() {
2888
+ if (!this._fields) {
2889
+ throw new Error("Fields must be configured (use fieldsFromDict or fieldsFromArray)");
2890
+ }
2891
+ if (Array.isArray(this._fields) && this._fields.length === 0) {
2892
+ throw new Error("Fields must not be empty");
2893
+ }
2894
+ if (!Array.isArray(this._fields) && Object.keys(this._fields).length === 0) {
2895
+ throw new Error("Fields must not be empty");
2896
+ }
2897
+ const config = {
2898
+ separator: this._separator,
2899
+ fields: this._fields
2900
+ };
2901
+ if (this._encoding !== void 0) {
2902
+ config.encoding = this._encoding;
2903
+ }
2904
+ if (this._addSyncedAtColumn !== void 0) {
2905
+ config.addSyncedAtColumn = this._addSyncedAtColumn;
2906
+ }
2907
+ return config;
2908
+ }
2909
+ buildStreamConfig(streamId) {
2910
+ const csvConfig = this.build();
2911
+ const options = {};
2912
+ if (this._replicationMethod !== void 0) {
2913
+ options.replicationMethod = this._replicationMethod;
2914
+ }
2915
+ if (this._primaryKeys !== void 0) {
2916
+ options.primaryKeys = this._primaryKeys;
2917
+ }
2918
+ return createCsvStreamConfig(streamId, csvConfig, options);
2919
+ }
2920
+ };
2921
+
2922
+ // src/sdk/file-processing/file-tap.ts
2923
+ var FileTap = class _FileTap extends Tap {
2924
+ entries;
2925
+ constructor(target, config, stateProvider, entries) {
2926
+ super(target, config, stateProvider);
2927
+ this.entries = entries;
2928
+ }
2929
+ /**
2930
+ * Convenience constructor for the common case where all configs share the same file path.
2931
+ */
2932
+ static fromConfigs(target, config, stateProvider, fileConfigs, sharedFilePath) {
2933
+ const entries = fileConfigs.map((c) => ({
2934
+ config: c,
2935
+ filePath: sharedFilePath
2936
+ }));
2937
+ return new _FileTap(target, config, stateProvider, entries);
2938
+ }
2939
+ async init() {
2940
+ for (const entry of this.entries) {
2941
+ const stream = new FileStream(
2942
+ entry.config,
2943
+ entry.filePath,
2944
+ this.tapState,
2945
+ this.target
2946
+ );
2947
+ this.streams.push(stream);
2948
+ }
2949
+ }
2950
+ };
2951
+
2952
+ // src/sdk/file-processing/file-patterns.ts
2953
+ var VariableExtractors = class {
2954
+ /**
2955
+ * Returns the filename as a variable.
2956
+ */
2957
+ static filename() {
2958
+ return (filename) => ({
2959
+ fileName: filename
2960
+ });
2961
+ }
2962
+ /**
2963
+ * Extracts named capture groups from a regex pattern.
2964
+ */
2965
+ static regex(pattern) {
2966
+ return (filename) => {
2967
+ const match = filename.match(pattern);
2968
+ return match?.groups ?? {};
2969
+ };
2970
+ }
2971
+ /**
2972
+ * Combines multiple extraction functions.
2973
+ */
2974
+ static combine(...extractors) {
2975
+ return (filename) => {
2976
+ return extractors.reduce((acc, extractor) => {
2977
+ return { ...acc, ...extractor(filename) };
2978
+ }, {});
2979
+ };
2980
+ }
2981
+ };
2982
+ var FilePatterns = class {
2983
+ /**
2984
+ * Creates a validator for files starting with a specific prefix (case-insensitive).
2985
+ */
2986
+ static startsWith(prefix) {
2987
+ return (filename) => filename.toLowerCase().startsWith(prefix.toLowerCase());
2988
+ }
2989
+ /**
2990
+ * Creates a validator for files matching a regex pattern.
2991
+ */
2992
+ static regex(pattern) {
2993
+ return (filename) => pattern.test(filename);
2994
+ }
2995
+ /**
2996
+ * Combines multiple validators with AND logic.
2997
+ */
2998
+ static and(...validators) {
2999
+ return (filename) => validators.every((validator) => validator(filename));
3000
+ }
3001
+ /**
3002
+ * Combines multiple validators with OR logic.
3003
+ */
3004
+ static or(...validators) {
3005
+ return (filename) => validators.some((validator) => validator(filename));
3006
+ }
3007
+ };
3008
+
3009
+ // src/services/sftp.ts
3010
+ import Client from "ssh2-sftp-client";
3011
+ import { default as default2 } from "ssh2-sftp-client";
3012
+ var SftpService = new Client();
3013
+
3014
+ // src/services/cloud-storage.ts
3015
+ import { Storage } from "@google-cloud/storage";
3016
+ import { format as format6 } from "util";
3017
+ import * as pathModule from "path";
3018
+ import { existsSync, mkdirSync, unlinkSync as unlinkSync2 } from "fs";
3019
+ import { randomUUID } from "crypto";
3020
+ var logPrefix3 = "[CloudStorageService]";
3021
+ var tmpDir = "tmp";
3022
+ var CloudStorageService = class {
3023
+ storage;
3024
+ bucket;
3025
+ processedSuffix;
3026
+ supportedExtensions;
3027
+ path;
3028
+ constructor(bucketName, path2, opts) {
3029
+ this.storage = new Storage({ retryOptions: { autoRetry: true, maxRetries: 20 } });
3030
+ this.bucket = this.storage.bucket(bucketName);
3031
+ this.path = path2;
3032
+ this.processedSuffix = opts?.processedSuffix ?? ".processed";
3033
+ this.supportedExtensions = opts?.supportedExtensions ?? [];
3034
+ }
3035
+ /**
3036
+ * Lists all files in the bucket with an optional prefix.
3037
+ */
3038
+ async listFiles(prefix) {
3039
+ const effectivePrefix = prefix || this.path;
3040
+ const [files] = await this.bucket.getFiles(
3041
+ effectivePrefix ? { prefix: effectivePrefix } : void 0
3042
+ );
3043
+ return files.map((f) => f.name);
3044
+ }
3045
+ /**
3046
+ * Returns files that haven't been marked as processed.
3047
+ * Filters by supported extensions if configured.
3048
+ */
3049
+ async getUnprocessedFiles() {
3050
+ const allFiles = await this.listFiles();
3051
+ const markerFiles = new Set(
3052
+ allFiles.filter((f) => f.endsWith(this.processedSuffix)).map((f) => f.replace(this.processedSuffix, ""))
3053
+ );
3054
+ return allFiles.filter((f) => {
3055
+ if (f.endsWith(this.processedSuffix)) return false;
3056
+ if (f.endsWith("/")) return false;
3057
+ if (this.supportedExtensions.length > 0) {
3058
+ const ext = pathModule.extname(f).toLowerCase();
3059
+ if (!this.supportedExtensions.includes(ext)) return false;
3060
+ }
3061
+ return !markerFiles.has(f);
3062
+ });
3063
+ }
3064
+ /**
3065
+ * Creates a marker file to indicate a file has been processed.
3066
+ */
3067
+ async createMarkerFile(fileName) {
3068
+ const markerFileName = `${fileName}${this.processedSuffix}`;
3069
+ try {
3070
+ const file = this.bucket.file(markerFileName);
3071
+ await file.save(format6("Marked file %s as processed", fileName));
3072
+ logger.info(`${logPrefix3} Marker file ${markerFileName} created successfully.`);
3073
+ } catch (err) {
3074
+ if (err instanceof Error) {
3075
+ throw new Error(format6(`error while writing marker file=%s, err:%s`, markerFileName, err.message));
3076
+ }
3077
+ throw err;
3078
+ }
3079
+ }
3080
+ /**
3081
+ * Downloads a file from GCS to a local tmp directory.
3082
+ * Returns the local path of the downloaded file.
3083
+ */
3084
+ async downloadFile(filePath, fileName) {
3085
+ const file = this.bucket.file(filePath);
3086
+ const tmpDirPath = pathModule.join(process.cwd(), tmpDir);
3087
+ if (!existsSync(tmpDirPath)) {
3088
+ mkdirSync(tmpDirPath);
3089
+ }
3090
+ const destFilename = pathModule.join(process.cwd(), tmpDir, fileName);
3091
+ try {
3092
+ if (existsSync(destFilename)) {
3093
+ unlinkSync2(destFilename);
3094
+ }
3095
+ await file.download({ destination: destFilename });
3096
+ logger.info(`${logPrefix3} Downloaded ${fileName} to ${destFilename}`);
3097
+ return destFilename;
3098
+ } catch (err) {
3099
+ if (err instanceof Error) {
3100
+ throw new Error(format6(
3101
+ `can't download file=%s from bucket, err:%s`,
3102
+ fileName,
3103
+ err.message
3104
+ ));
3105
+ }
3106
+ throw err;
3107
+ }
3108
+ }
3109
+ /**
3110
+ * Uploads a local file to GCS.
3111
+ */
3112
+ async uploadFile(localPath, destPath) {
3113
+ try {
3114
+ logger.info(`${logPrefix3} preparing to upload '%s' to '%s'`, localPath, destPath);
3115
+ const [file] = await this.bucket.upload(localPath, { destination: destPath });
3116
+ logger.info(`${logPrefix3} file %s has been successfully uploaded into %s`, localPath, destPath);
3117
+ return file;
3118
+ } catch (err) {
3119
+ if (err instanceof Error) {
3120
+ throw new Error(format6(`error while uploading file file=%s into path=%s, err:%s`, localPath, destPath, err.message));
3121
+ }
3122
+ throw err;
3123
+ }
3124
+ }
3125
+ /**
3126
+ * Reads a GCS object and returns its contents as a UTF-8 string.
3127
+ */
3128
+ async readObjectAsString(objectPath) {
3129
+ try {
3130
+ await this.bucket.get({ autoCreate: false });
3131
+ const fileRef = this.bucket.file(objectPath);
3132
+ const [exists] = await fileRef.exists();
3133
+ if (!exists) {
3134
+ throw new Error(`GCS object not found: gs://${this.bucket.name}/${objectPath}`);
3135
+ }
3136
+ const [contents] = await fileRef.download();
3137
+ return contents.toString("utf8");
3138
+ } catch (err) {
3139
+ throw new Error(format6(`error reading GCS object gs://${this.bucket.name}/${objectPath}, err: %s`, err?.message));
3140
+ }
3141
+ }
3142
+ /**
3143
+ * Writes a string to a GCS object with JSON content type.
3144
+ */
3145
+ async writeStringObject(objectPath, contents) {
3146
+ try {
3147
+ await this.bucket.get({ autoCreate: false });
3148
+ const fileRef = this.bucket.file(objectPath);
3149
+ await fileRef.save(contents, { contentType: "application/json" });
3150
+ logger.info(`Uploaded object to gs://${this.bucket.name}/${objectPath}`);
3151
+ } catch (err) {
3152
+ throw new Error(format6(`error writing GCS object gs://${this.bucket.name}/${objectPath}, err: %s`, err?.message));
3153
+ }
3154
+ }
3155
+ /**
3156
+ * Uploads a local file to the bucket with a unique name based on prefix, streamId, and UUID.
3157
+ * Files are stored under `<prefix>/<run-id>/` when the RUN_ID env var is set,
3158
+ * or `<prefix>/default/` otherwise.
3159
+ * Returns the GCS File reference.
3160
+ */
3161
+ async uploadFileWithUniqueName(filePath, prefix, streamId) {
3162
+ try {
3163
+ await this.bucket.get({ autoCreate: false });
3164
+ const runFolder = process.env.RUN_ID ?? "default";
3165
+ const destinationFileName = `${prefix}/${runFolder}/${streamId}-${randomUUID()}.jsonnl`;
3166
+ await this.bucket.upload(filePath, {
3167
+ destination: destinationFileName
3168
+ });
3169
+ logger.info(`Uploaded ${filePath} into ${this.bucket.name}/${destinationFileName} GCS File`);
3170
+ return this.bucket.file(destinationFileName);
3171
+ } catch (err) {
3172
+ logger.error(`Issue when uploading file into GCS bucket for stream: ${streamId}
3173
+
3174
+ Error: ${err.message}
3175
+ Stack: ${err.stack}
3176
+ Code: ${err.code}
3177
+ `);
3178
+ throw err;
3179
+ }
3180
+ }
3181
+ };
3182
+
3183
+ // src/services/zip.ts
3184
+ import * as fse from "fs-extra";
3185
+ import decompress from "decompress";
3186
+ var unzip = async (zipFilePath, extractedPath) => {
3187
+ await fse.ensureDir(extractedPath);
3188
+ await decompress(zipFilePath, extractedPath);
3189
+ return extractedPath;
3190
+ };
3191
+
2138
3192
  // src/targets/bigquery/helpers.ts
2139
3193
  var safeColumnName = (key) => {
2140
3194
  let returnString = key;
@@ -2171,8 +3225,8 @@ import Decimal from "decimal.js";
2171
3225
  import dayjs5 from "dayjs";
2172
3226
  var validateDateRange = (record, schema) => {
2173
3227
  Object.keys(schema).forEach((key) => {
2174
- const { format: format6 } = schema[key];
2175
- if (format6 === "date-time") {
3228
+ const { format: format8 } = schema[key];
3229
+ if (format8 === "date-time") {
2176
3230
  if (record[key]) {
2177
3231
  const formattedDate = dayjs5(record[key]).format(defaultDateTimeFormat);
2178
3232
  const isNotTooMuchInTheFuture = dayjs5(record[key]).isBefore(dayjs5("9999-12-31 23:59:59.999999"));
@@ -2256,96 +3310,6 @@ var BqClientHolder = class {
2256
3310
  }
2257
3311
  };
2258
3312
 
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
3313
  // src/targets/bigquery/service/dbSync.ts
2350
3314
  import chunk from "lodash/chunk.js";
2351
3315
  import { Semaphore as Semaphore2 } from "async-mutex";
@@ -2386,21 +3350,21 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
2386
3350
  safeColumnName = safeColumnName;
2387
3351
  getWarehouseTypeFromJSONSchema = (definition) => {
2388
3352
  const type = definition.type;
2389
- const format6 = definition.format;
3353
+ const format8 = definition.format;
2390
3354
  let typeArr;
2391
3355
  if (type instanceof Array) {
2392
3356
  typeArr = type;
2393
3357
  } else {
2394
3358
  typeArr = [type];
2395
3359
  }
2396
- if (format6) {
2397
- switch (format6) {
3360
+ if (format8) {
3361
+ switch (format8) {
2398
3362
  case "date-time":
2399
3363
  return "TIMESTAMP";
2400
3364
  case "json":
2401
3365
  return "STRING";
2402
3366
  default:
2403
- throw new Error(`StreamId: ${this.streamId} - Unsupported format: ${format6}`);
3367
+ throw new Error(`StreamId: ${this.streamId} - Unsupported format: ${format8}`);
2404
3368
  }
2405
3369
  } else if (typeArr.includes("number")) {
2406
3370
  return "NUMERIC";
@@ -2496,6 +3460,13 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
2496
3460
  ...this.getMergeQueries()
2497
3461
  ];
2498
3462
  };
3463
+ getAppendQueries = () => {
3464
+ const columnUnsafeToSafeMapping = this.renamedColumnStore.getUnsafeToSafeColumnMapping(this.streamId);
3465
+ const columns = Object.keys(columnUnsafeToSafeMapping).map((unsafeKey) => `\`${columnUnsafeToSafeMapping[unsafeKey]}\``).join(`, `);
3466
+ return [
3467
+ `INSERT INTO ${this.sqlTableId} (${columns}) SELECT ${columns} FROM ${this.sqlStagingTableId};`
3468
+ ];
3469
+ };
2499
3470
  createDatabaseAndSchemaIfNotExists = async (retryCount) => {
2500
3471
  try {
2501
3472
  await semaphore2.runExclusive(async () => {
@@ -2620,11 +3591,11 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
2620
3591
  };
2621
3592
  loadStreamInStagingArea = async (localFilePath) => {
2622
3593
  try {
2623
- const file = await uploadFileInBucket(
2624
- this.streamId,
2625
- this.config.loading_deck_gcs_bucket_name,
3594
+ const gcsService = new CloudStorageService(this.config.loading_deck_gcs_bucket_name);
3595
+ const file = await gcsService.uploadFileWithUniqueName(
3596
+ localFilePath,
2626
3597
  this.config.connector_id,
2627
- localFilePath
3598
+ this.streamId
2628
3599
  );
2629
3600
  const metadata = {
2630
3601
  sourceFormat: "NEWLINE_DELIMITED_JSON",
@@ -2633,6 +3604,17 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
2633
3604
  await this.loadGCSFileInTable(this.sqlStagingTableId, file, metadata);
2634
3605
  } catch (err) {
2635
3606
  logger.error(`StreamId: ${this.streamId} - Error while uploading stream.`);
3607
+ if (err.code < 500) {
3608
+ await haltAndCatchFire(
3609
+ `unauthorized`,
3610
+ `We couldn't connect to your Cloud Storage bucket \u{1F614}
3611
+
3612
+ The error from Google is: '${err.message}' \u{1F440}
3613
+
3614
+ Could you troubleshoot your Cloud Storage configuration in Google Cloud and sync again the source? \u{1F64F}`,
3615
+ `Got error from cloud storage lib`
3616
+ );
3617
+ }
2636
3618
  throw err;
2637
3619
  }
2638
3620
  };
@@ -2760,47 +3742,16 @@ var BigQueryTarget = class extends ITarget {
2760
3742
  convertNumberIntoDecimal = convertNumberIntoDecimal;
2761
3743
  };
2762
3744
 
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
3745
  // src/state-providers/gcs/main.ts
2797
- import { format as format5 } from "util";
3746
+ import { format as format7 } from "util";
2798
3747
  var GCSStateProvider = class {
2799
3748
  bucketName;
2800
3749
  connectorId;
3750
+ service;
2801
3751
  constructor(connectorId, bucketName) {
2802
3752
  this.connectorId = connectorId;
2803
3753
  this.bucketName = bucketName;
3754
+ this.service = new CloudStorageService(bucketName);
2804
3755
  }
2805
3756
  getObjectPath() {
2806
3757
  return `${this.connectorId}/state.json`;
@@ -2809,11 +3760,11 @@ var GCSStateProvider = class {
2809
3760
  const objectPath = this.getObjectPath();
2810
3761
  try {
2811
3762
  logger.info(`\u{1F4C1} Loading state from gs://${this.bucketName}/${objectPath}`);
2812
- const raw = await readObjectAsString(this.bucketName, objectPath);
3763
+ const raw = await this.service.readObjectAsString(objectPath);
2813
3764
  const parsed = JSON.parse(raw);
2814
3765
  return { state: parsed };
2815
3766
  } catch (err) {
2816
- logger.warn(format5(`error loading state from gs://${this.bucketName}/${objectPath}, will start with empty state, err: %s`, err?.message));
3767
+ logger.warn(format7(`error loading state from gs://${this.bucketName}/${objectPath}, will start with empty state, err: %s`, err?.message));
2817
3768
  return { state: void 0 };
2818
3769
  }
2819
3770
  }
@@ -2821,10 +3772,10 @@ var GCSStateProvider = class {
2821
3772
  const objectPath = this.getObjectPath();
2822
3773
  try {
2823
3774
  logger.info(`\u{1F4DD} Writing state to gs://${this.bucketName}/${objectPath}`);
2824
- await writeStringObject(this.bucketName, objectPath, state);
3775
+ await this.service.writeStringObject(objectPath, state);
2825
3776
  } 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));
3777
+ logger.error(format7(`\u{1F4A5} Error while writing state to gs://${this.bucketName}/${objectPath}, err: %s`, err?.message));
3778
+ throw new Error(format7(`error writing state to gs://${this.bucketName}/${objectPath}, err: %s`, err?.message));
2828
3779
  }
2829
3780
  }
2830
3781
  };
@@ -2834,9 +3785,16 @@ export {
2834
3785
  Authenticator,
2835
3786
  BATCH_INTERVAL_MS,
2836
3787
  BigQueryTarget,
3788
+ CloudStorageService,
2837
3789
  CounterMetric,
3790
+ CsvExtractionConfigBuilder,
2838
3791
  DEFAULT_MAX_CONCURRENT_STREAMS,
2839
3792
  EXECUTION_TIME_METRIC_NAME,
3793
+ ExcelExtractionConfigBuilder,
3794
+ FileFormat,
3795
+ FilePatterns,
3796
+ FileStream,
3797
+ FileTap,
2840
3798
  GCSStateProvider,
2841
3799
  ITarget,
2842
3800
  MissingFieldInSchemaError,
@@ -2849,16 +3807,34 @@ export {
2849
3807
  ReplicationMethodMessage,
2850
3808
  SchemaMessage,
2851
3809
  SchemaValidationError,
3810
+ default2 as SftpClient,
3811
+ SftpService,
2852
3812
  StateMessage,
2853
3813
  StateService,
2854
3814
  Stream,
2855
3815
  StreamWarehouseSyncService,
2856
3816
  Tap,
3817
+ VariableExtractors,
2857
3818
  _greaterThan,
2858
3819
  addWhalyFields,
3820
+ checkCsvHeaderRow,
3821
+ countCsvLines,
3822
+ createCellReference,
3823
+ createCsvStreamConfig,
3824
+ createExcelGenerator,
3825
+ createExcelStreamConfig,
2859
3826
  createTemporaryFileStream,
3827
+ csvFieldsToJsonSchema,
3828
+ excelColumnToIndex,
3829
+ excelFieldsToJsonSchema,
3830
+ excelRowToIndex,
3831
+ extractExcelRows,
3832
+ extractPrimaryKeysFromCsvConfig,
3833
+ extractPrimaryKeysFromExcelFields,
2860
3834
  extractStateForStream,
3835
+ fieldTypeToJsonSchema,
2861
3836
  finalizeStateProgressMarkers,
3837
+ findAllMatchingConfigs,
2862
3838
  flattenSchema,
2863
3839
  getAllMetrics,
2864
3840
  getAxiosInstance,
@@ -2871,14 +3847,23 @@ export {
2871
3847
  gracefulExit,
2872
3848
  haltAndCatchFire,
2873
3849
  incrementStreamState,
3850
+ indexToExcelColumn,
3851
+ indexToExcelRow,
2874
3852
  loadJson,
2875
3853
  loadSchema,
2876
3854
  logger,
3855
+ parseCellReference,
2877
3856
  postFormDataApiCall,
2878
3857
  postJSONApiCall,
2879
3858
  postUrlEncodedApiCall,
2880
3859
  printMemoryFootprint,
3860
+ processFileStreams,
2881
3861
  removeParasiteProperties,
2882
- safePath
3862
+ rowGeneratorFromCsv,
3863
+ safePath,
3864
+ unzip,
3865
+ validateExcelConfig,
3866
+ writeDataToCsv,
3867
+ writeGeneratorToCSV
2883
3868
  };
2884
3869
  //# sourceMappingURL=index.mjs.map