@whaly/connector-sdk 0.3.4 → 0.3.6

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
@@ -279,13 +279,26 @@ var postUrlEncodedApiCall = async (endpoint, config, payload) => {
279
279
  return response.data;
280
280
  } catch (err) {
281
281
  if (err.response.status > 400) {
282
- throw new Error(`We got a \`${err.response.status}\` status code from endpoint: \`${endpoint}\`.
283
-
282
+ throw new Error(`We got a \`${err.response.status}\` status code from endpoint: \`${endpoint}\`.
283
+
284
284
  Response body from the server: ${JSON.stringify(err.response.data)}`);
285
285
  }
286
286
  throw err;
287
287
  }
288
288
  };
289
+ async function collectPaginated(options) {
290
+ const { initialUrl, fetchPage, getRecords, getNextUrl } = options;
291
+ const results = [];
292
+ let currentUrl = initialUrl;
293
+ while (true) {
294
+ const response = await fetchPage(currentUrl);
295
+ results.push(...getRecords(response));
296
+ const nextUrl = getNextUrl(response);
297
+ if (nextUrl === void 0) break;
298
+ currentUrl = nextUrl;
299
+ }
300
+ return results;
301
+ }
289
302
 
290
303
  // src/sdk/service/metric.ts
291
304
  var metricStore = {};
@@ -376,6 +389,157 @@ var haltAndCatchFire = async (errorType, errorText, errorDebugText) => {
376
389
  await gracefulExit(0);
377
390
  };
378
391
 
392
+ // src/sdk/service/dryRun.ts
393
+ function isDryRun() {
394
+ const val = process.env["DRY_RUN"];
395
+ return val !== void 0 && val !== "" && val !== "0" && val !== "false";
396
+ }
397
+ function getDryRunLimit() {
398
+ const val = process.env["DRY_RUN_LIMIT"];
399
+ if (!val) return void 0;
400
+ const n = parseInt(val, 10);
401
+ return isNaN(n) || n <= 0 ? void 0 : n;
402
+ }
403
+
404
+ // src/sdk/service/apiEndpoint.ts
405
+ var ENV_VAR = "WLY_API_ENDPOINT";
406
+ function getApiEndpoint(explicit) {
407
+ const endpoint = explicit ?? process.env[ENV_VAR];
408
+ if (!endpoint) {
409
+ throw new Error(
410
+ `No API endpoint provided. Either pass "apiEndpoint" in config or set the ${ENV_VAR} environment variable.`
411
+ );
412
+ }
413
+ return endpoint;
414
+ }
415
+
416
+ // src/sdk/service/serviceAccountKey.ts
417
+ var ENV_VAR2 = "WLY_SERVICE_ACCOUNT_KEY";
418
+ function getServiceAccountKey(explicit) {
419
+ const key = explicit ?? process.env[ENV_VAR2];
420
+ if (!key) {
421
+ throw new Error(
422
+ `No service account key provided. Either pass "serviceAccountKey" in config or set the ${ENV_VAR2} environment variable.`
423
+ );
424
+ }
425
+ return key;
426
+ }
427
+
428
+ // src/sdk/service/cdnId.ts
429
+ var ENV_VAR3 = "WLY_CDN_ID";
430
+ function getCdnId(explicit) {
431
+ const cdnId = explicit ?? process.env[ENV_VAR3];
432
+ if (!cdnId) {
433
+ throw new Error(
434
+ `No CDN ID provided. Either pass "cdnId" in config or set the ${ENV_VAR3} environment variable.`
435
+ );
436
+ }
437
+ return cdnId;
438
+ }
439
+
440
+ // src/sdk/service/env.ts
441
+ import dotenv from "dotenv";
442
+ var dotenvLoaded = false;
443
+ function ensureDotenv() {
444
+ if (!dotenvLoaded) {
445
+ dotenv.config();
446
+ dotenvLoaded = true;
447
+ }
448
+ }
449
+ function requireEnv(name) {
450
+ ensureDotenv();
451
+ const value = process.env[name];
452
+ if (!value) {
453
+ throw new Error(`Missing required environment variable: ${name}`);
454
+ }
455
+ return value;
456
+ }
457
+ function optionalEnv(name, defaultValue) {
458
+ ensureDotenv();
459
+ return process.env[name] ?? defaultValue;
460
+ }
461
+
462
+ // src/sdk/service/concurrency.ts
463
+ async function runWithConcurrency(tasks, concurrency) {
464
+ if (concurrency < 1) {
465
+ throw new Error(`concurrency must be >= 1, got ${concurrency}`);
466
+ }
467
+ const results = new Array(tasks.length);
468
+ let nextIndex = 0;
469
+ async function worker() {
470
+ while (nextIndex < tasks.length) {
471
+ const index = nextIndex++;
472
+ const task = tasks[index];
473
+ if (task) {
474
+ results[index] = await task();
475
+ }
476
+ }
477
+ }
478
+ const workers = Array.from(
479
+ { length: Math.min(concurrency, tasks.length) },
480
+ () => worker()
481
+ );
482
+ await Promise.all(workers);
483
+ return results;
484
+ }
485
+ async function processFromAsyncIterable(iterable, processor, concurrency, onError) {
486
+ if (concurrency < 1) {
487
+ throw new Error(`concurrency must be >= 1, got ${concurrency}`);
488
+ }
489
+ const iterator = iterable[Symbol.asyncIterator]();
490
+ let stopped = false;
491
+ async function worker() {
492
+ while (!stopped) {
493
+ const { value, done } = await iterator.next();
494
+ if (done) break;
495
+ try {
496
+ const result = await processor(value);
497
+ if (result === "stop") {
498
+ stopped = true;
499
+ break;
500
+ }
501
+ } catch (err) {
502
+ if (onError) {
503
+ onError(value, err);
504
+ } else {
505
+ stopped = true;
506
+ throw err;
507
+ }
508
+ }
509
+ }
510
+ }
511
+ const workers = Array.from({ length: concurrency }, () => worker());
512
+ await Promise.all(workers);
513
+ }
514
+
515
+ // src/sdk/service/mime.ts
516
+ import path from "path";
517
+ var MIME_TYPES = {
518
+ ".webp": "image/webp",
519
+ ".png": "image/png",
520
+ ".jpg": "image/jpeg",
521
+ ".jpeg": "image/jpeg",
522
+ ".gif": "image/gif",
523
+ ".svg": "image/svg+xml",
524
+ ".avif": "image/avif",
525
+ ".bmp": "image/bmp",
526
+ ".tiff": "image/tiff",
527
+ ".tif": "image/tiff",
528
+ ".ico": "image/x-icon",
529
+ ".pdf": "application/pdf",
530
+ ".json": "application/json",
531
+ ".csv": "text/csv",
532
+ ".xml": "application/xml",
533
+ ".zip": "application/zip",
534
+ ".gz": "application/gzip",
535
+ ".xls": "application/vnd.ms-excel",
536
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
537
+ };
538
+ function getMimeType(filePathOrExt) {
539
+ const ext = filePathOrExt.startsWith(".") ? filePathOrExt.toLowerCase() : path.extname(filePathOrExt).toLowerCase();
540
+ return MIME_TYPES[ext] ?? "application/octet-stream";
541
+ }
542
+
379
543
  // src/sdk/models/replication.ts
380
544
  var ReplicationMethod = /* @__PURE__ */ ((ReplicationMethod2) => {
381
545
  ReplicationMethod2["INCREMENTAL"] = "INCREMENTAL";
@@ -673,6 +837,9 @@ var Tap = class {
673
837
  this.tapState = cloneDeep(initialState.state);
674
838
  }
675
839
  await this.init();
840
+ if (isDryRun()) {
841
+ logger.info(`[DRY_RUN] mode active \u2014 no external writes will occur`);
842
+ }
676
843
  logger.info(`\u{1F680} Start syncing`);
677
844
  const state = StateService.getInstance().get();
678
845
  logger.info(`\u{1F4CD} Received state: %j`, state);
@@ -966,6 +1133,7 @@ var Stream = class {
966
1133
  // Private sync methods:
967
1134
  async _syncRecords(parent) {
968
1135
  let rowsSent = 0;
1136
+ const dryRunLimit = isDryRun() ? getDryRunLimit() : void 0;
969
1137
  let recordSyncedMetrics = [];
970
1138
  if (this.rowsSyncedMetricsConf.isEnabled() === true) {
971
1139
  recordSyncedMetrics = getCounterMetrics(
@@ -1007,6 +1175,10 @@ var Stream = class {
1007
1175
  metric.increment();
1008
1176
  });
1009
1177
  rowsSent += 1;
1178
+ if (dryRunLimit !== void 0 && rowsSent >= dryRunLimit) {
1179
+ logger.info(`\u{1F6D1} Stream: ${this.streamId} - DRY_RUN_LIMIT reached (${dryRunLimit} records), stopping.`);
1180
+ break;
1181
+ }
1010
1182
  if (rowsSent % 1e3 === 0) {
1011
1183
  const percentStr = this.totalRows ? ` (${Math.round(rowsSent / this.totalRows * 100)}%)` : "";
1012
1184
  logger.info(`\u23F3 Stream: ${this.streamId} - ${rowsSent} records synced so far...${percentStr}`);
@@ -1698,15 +1870,15 @@ import fs2 from "fs-extra";
1698
1870
  import uniqueId from "lodash/uniqueId.js";
1699
1871
  var createTemporaryFileStream = (streamId) => {
1700
1872
  fs2.ensureDirSync("./tmp");
1701
- const path2 = `./tmp/${safePath(streamId)}-${uniqueId()}.tmp`;
1702
- const writeStream = fs2.createWriteStream(path2, { flags: "w" });
1873
+ const path7 = `./tmp/${safePath(streamId)}-${uniqueId()}.tmp`;
1874
+ const writeStream = fs2.createWriteStream(path7, { flags: "w" });
1703
1875
  return {
1704
1876
  stream: writeStream,
1705
- path: path2
1877
+ path: path7
1706
1878
  };
1707
1879
  };
1708
- function safePath(path2) {
1709
- let formattedPath = path2;
1880
+ function safePath(path7) {
1881
+ let formattedPath = path7;
1710
1882
  formattedPath = formattedPath.replace(/^The\s+/gi, "");
1711
1883
  formattedPath = formattedPath.replace(/\s+(?:\(|\[)?Dis(?:c|k) (?:One|Two|Three|Four|Five|Six|Seven|Eight|Nine|Ten|\d)+(?:\)|\])?/gi, "");
1712
1884
  formattedPath = formattedPath.replace(/\s+/gi, "_");
@@ -1832,6 +2004,8 @@ var StreamState = class {
1832
2004
  // src/sdk/models/target/target.ts
1833
2005
  import { Semaphore } from "async-mutex";
1834
2006
  import Bluebird3 from "bluebird";
2007
+ import fs4 from "fs-extra";
2008
+ import path2 from "path";
1835
2009
  var semaphore = new Semaphore(1);
1836
2010
  var ITarget = class _ITarget {
1837
2011
  config;
@@ -1918,7 +2092,7 @@ var ITarget = class _ITarget {
1918
2092
  if (!stream) {
1919
2093
  throw new MissingSchemaError(`A record for stream:\`${streamId}\` was encountered before a corresponding schema.`, streamId);
1920
2094
  }
1921
- if (stream.getBatchedRowCount() % 1e4 === 0) {
2095
+ if (stream.getBatchedRowCount() % 1e4 === 0 && !isDryRun()) {
1922
2096
  await semaphore.runExclusive(async () => {
1923
2097
  const shouldUploadIntermediateBatch = this.shouldUploadIntermediateBatch(streamId);
1924
2098
  if (shouldUploadIntermediateBatch) {
@@ -2004,7 +2178,9 @@ var ITarget = class _ITarget {
2004
2178
  const { hasBreakingChanges, changes } = this.analyzeSchemaChanges(prevSchema, newFlattenedSchema, streamId);
2005
2179
  if (hasBreakingChanges) {
2006
2180
  logger.info(`\u{1F525} StreamId=${streamId} - Detected BREAKING schema changes: ${changes.join(", ")}. Loading batched records before applying new schema.`);
2007
- await _ITarget.uploadSingleStreamToWarehouse(prevStreamState);
2181
+ if (!isDryRun()) {
2182
+ await _ITarget.uploadSingleStreamToWarehouse(prevStreamState);
2183
+ }
2008
2184
  } else {
2009
2185
  logger.info(`\u2705 StreamId=${streamId} - Detected NON-BREAKING schema changes: ${changes.join(", ")}. Continuing without uploading batched records.`);
2010
2186
  }
@@ -2028,36 +2204,41 @@ var ITarget = class _ITarget {
2028
2204
  streamState.setSchema(schema);
2029
2205
  streamState.setCompiledValidateFn(this.ajv.compile({ type: "object", properties: schema }));
2030
2206
  const streamReplicationMethod = this.streams[streamId]?.getReplicationMethod();
2031
- if (message.keyProperties.length === 0 && streamReplicationMethod !== "APPEND" /* APPEND */) {
2207
+ if (message.keyProperties.length === 0 && streamReplicationMethod === "INCREMENTAL" /* INCREMENTAL */) {
2032
2208
  throw new Error(`StreamId: ${message.stream} - \`key_properties\` field is required and can't be empty in SCHEMA message.
2033
2209
  Received SCHEMA message: ${JSON.stringify(message)}`);
2034
2210
  }
2035
2211
  streamState.setKeyProperties(message.keyProperties);
2036
- await dbSync.updateSchemaInWarehouse(schema, message.keyProperties);
2037
- const {
2038
- database: databaseName,
2039
- schema: schemaName
2040
- } = this.config;
2041
- const tableName = this.genTableName(streamId);
2042
- const formattedRelationshipMessage = {
2043
- ...message,
2044
- keyProperties: message.keyProperties.map((k) => {
2045
- const translation = streamState.getDbSync().renamedColumnStore.getColumnTranslation(message.stream, k);
2046
- if (translation) {
2047
- return translation;
2048
- }
2049
- return k;
2050
- })
2051
- };
2052
- await Bluebird3.map(this.schemaHooks, async (hook) => {
2053
- const input = {
2054
- databaseName,
2055
- schemaName,
2056
- tableName,
2057
- message: formattedRelationshipMessage
2212
+ dbSync.renamedColumnStore.computeColumnNameForStream(streamId, schema);
2213
+ if (!isDryRun()) {
2214
+ await dbSync.updateSchemaInWarehouse(schema, message.keyProperties);
2215
+ const {
2216
+ database: databaseName,
2217
+ schema: schemaName
2218
+ } = this.config;
2219
+ const tableName = this.genTableName(streamId);
2220
+ const formattedRelationshipMessage = {
2221
+ ...message,
2222
+ keyProperties: message.keyProperties.map((k) => {
2223
+ const translation = streamState.getDbSync().renamedColumnStore.getColumnTranslation(message.stream, k);
2224
+ if (translation) {
2225
+ return translation;
2226
+ }
2227
+ return k;
2228
+ })
2058
2229
  };
2059
- await hook.writeSchema(input);
2060
- }, { concurrency: 3 });
2230
+ await Bluebird3.map(this.schemaHooks, async (hook) => {
2231
+ const input = {
2232
+ databaseName,
2233
+ schemaName,
2234
+ tableName,
2235
+ message: formattedRelationshipMessage
2236
+ };
2237
+ await hook.writeSchema(input);
2238
+ }, { concurrency: 3 });
2239
+ } else {
2240
+ logger.info(`[DRY_RUN] Stream: ${streamId} - Skipping warehouse schema update and schema hooks`);
2241
+ }
2061
2242
  return Promise.resolve();
2062
2243
  } catch (err) {
2063
2244
  logger.error(`Error when handling schema event.`, err);
@@ -2078,6 +2259,29 @@ var ITarget = class _ITarget {
2078
2259
  };
2079
2260
  complete = async () => {
2080
2261
  try {
2262
+ if (isDryRun()) {
2263
+ logger.info(`[DRY_RUN] Data Extraction is complete. Copying tmp files to out/ instead of loading to warehouse.`);
2264
+ const outDir = path2.resolve("out");
2265
+ await fs4.ensureDir(outDir);
2266
+ for (const streamId of Object.keys(this.streams)) {
2267
+ const streamState = this.streams[streamId];
2268
+ if (!streamState || streamState.getBatchedRowCount() === 0) continue;
2269
+ const fileToLoad = streamState.getFileToLoad();
2270
+ await new Promise((resolve, reject) => {
2271
+ fileToLoad.stream.end(() => {
2272
+ fileToLoad.stream.close((err) => {
2273
+ if (err) reject(err);
2274
+ else resolve();
2275
+ });
2276
+ });
2277
+ });
2278
+ const destPath = path2.join(outDir, `${safePath(streamId)}.ndjson`);
2279
+ await fs4.copy(fileToLoad.path, destPath);
2280
+ logger.info(`[DRY_RUN] Stream: ${streamId} - Copied ${streamState.getBatchedRowCount()} records to ${destPath}`);
2281
+ await fs4.remove(fileToLoad.path);
2282
+ }
2283
+ return;
2284
+ }
2081
2285
  logger.info(`\u{1F44D} Data Extraction is complete. Will load remaining batched stream records.`);
2082
2286
  await this.loadAllStreamsInWarehouse({ isFinalLoad: true });
2083
2287
  return Promise.resolve();
@@ -2216,7 +2420,7 @@ function extractPrimaryKeysFromExcelFields(fields) {
2216
2420
 
2217
2421
  // src/sdk/file-processing/excel/reader.ts
2218
2422
  import XLSX from "xlsx";
2219
- import path from "path";
2423
+ import path3 from "path";
2220
2424
  var excelColumnToIndex = (columnLetter) => {
2221
2425
  const letters = columnLetter.toUpperCase();
2222
2426
  let index = 0;
@@ -2375,7 +2579,7 @@ var validateExcelConfig = (config) => {
2375
2579
  }
2376
2580
  };
2377
2581
  var findAllMatchingConfigs = (filename, configs) => {
2378
- const fileInfo = path.parse(filename);
2582
+ const fileInfo = path3.parse(filename);
2379
2583
  return configs.filter(
2380
2584
  (config) => config.fileNameValidator(fileInfo.base) && config.extension === fileInfo.ext.replace(".", "")
2381
2585
  );
@@ -2510,7 +2714,7 @@ var formatHexComparison = (expected, actual) => {
2510
2714
  Expected: "${expected}" (${expectedInfo.length} chars) [${expectedInfo.hex}]
2511
2715
  Actual: "${actual}" (${actualInfo.length} chars) [${actualInfo.hex}]`;
2512
2716
  };
2513
- async function* rowGeneratorFromCsv(path2, fileConfig) {
2717
+ async function* rowGeneratorFromCsv(path7, fileConfig) {
2514
2718
  const isGeneratorConfigArray = Array.isArray(fileConfig.fields);
2515
2719
  const configKeys = isGeneratorConfigArray ? [] : Object.keys(fileConfig.fields);
2516
2720
  const csvOptions = { separator: fileConfig.separator };
@@ -2522,10 +2726,10 @@ async function* rowGeneratorFromCsv(path2, fileConfig) {
2522
2726
  let finalInputStream;
2523
2727
  const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
2524
2728
  if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
2525
- initialReadStream = createReadStream(path2);
2729
+ initialReadStream = createReadStream(path7);
2526
2730
  finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
2527
2731
  } else {
2528
- initialReadStream = createReadStream(path2);
2732
+ initialReadStream = createReadStream(path7);
2529
2733
  finalInputStream = initialReadStream.pipe(createStripBomTransform());
2530
2734
  }
2531
2735
  finalInputStream.pipe(csvStream);
@@ -2567,7 +2771,7 @@ async function countCsvLines(filePath) {
2567
2771
  }).on("end", () => resolve(Math.max(0, newlines - 1))).on("error", reject);
2568
2772
  });
2569
2773
  }
2570
- var checkCsvHeaderRow = async (path2, fileConfig) => {
2774
+ var checkCsvHeaderRow = async (path7, fileConfig) => {
2571
2775
  return new Promise((resolve, reject) => {
2572
2776
  const colsWithParsingConfig = Object.keys(fileConfig.fields).map((key) => key.trim());
2573
2777
  console.debug(`${logPrefix} CSV Expected columns:`, colsWithParsingConfig.map((col) => {
@@ -2584,10 +2788,10 @@ var checkCsvHeaderRow = async (path2, fileConfig) => {
2584
2788
  let finalInputStream;
2585
2789
  const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
2586
2790
  if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
2587
- initialReadStream = createReadStream(path2);
2791
+ initialReadStream = createReadStream(path7);
2588
2792
  finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
2589
2793
  } else {
2590
- initialReadStream = createReadStream(path2);
2794
+ initialReadStream = createReadStream(path7);
2591
2795
  finalInputStream = initialReadStream.pipe(createStripBomTransform());
2592
2796
  }
2593
2797
  let isFirstLine = true;
@@ -2613,7 +2817,7 @@ var checkCsvHeaderRow = async (path2, fileConfig) => {
2613
2817
  i,
2614
2818
  col,
2615
2819
  headers[i] || "<undefined>",
2616
- path2
2820
+ path7
2617
2821
  ));
2618
2822
  }
2619
2823
  });
@@ -2630,7 +2834,7 @@ var checkCsvHeaderRow = async (path2, fileConfig) => {
2630
2834
  throw new Error(format4(
2631
2835
  `CSV missing expected column '%s' in file: %s`,
2632
2836
  col,
2633
- path2
2837
+ path7
2634
2838
  ));
2635
2839
  }
2636
2840
  });
@@ -2656,7 +2860,7 @@ var writeDataToCsv = async (fileName, data) => {
2656
2860
  try {
2657
2861
  const firstRow = data[0];
2658
2862
  if (!firstRow) {
2659
- console.log(`${logPrefix2} No data to write to CSV file=%s`, fileName);
2863
+ logger.info(`${logPrefix2} No data to write to CSV file=%s`, fileName);
2660
2864
  return;
2661
2865
  }
2662
2866
  const csvWriter = createObjectCsvWriter({
@@ -2666,7 +2870,7 @@ var writeDataToCsv = async (fileName, data) => {
2666
2870
  })
2667
2871
  });
2668
2872
  await csvWriter.writeRecords(data);
2669
- console.log(`${logPrefix2} CSV file=%s written successfully`, fileName);
2873
+ logger.info(`${logPrefix2} CSV file=%s written successfully`, fileName);
2670
2874
  } catch (err) {
2671
2875
  if (err instanceof Error) {
2672
2876
  throw new Error(format5(`error while writing csv file=%s, err: %s`, fileName, err.message));
@@ -2674,15 +2878,17 @@ var writeDataToCsv = async (fileName, data) => {
2674
2878
  throw err;
2675
2879
  }
2676
2880
  };
2677
- var writeGeneratorToCSV = async (generator, outputFileName) => {
2881
+ var writeGeneratorToCSV = async (generator, outputFileName, options) => {
2882
+ const batchSize = options?.batchSize ?? 1e4;
2883
+ const alwaysQuote = options?.alwaysQuote ?? true;
2678
2884
  try {
2679
2885
  let rowCount = 0;
2680
2886
  const firstDataRow = (await generator.next()).value;
2681
2887
  if (!firstDataRow) {
2682
- console.log(`${logPrefix2} [%s] first data row of csv empty, skipping`, outputFileName);
2888
+ logger.info(`${logPrefix2} [%s] first data row of csv empty, skipping`, outputFileName);
2683
2889
  return 0;
2684
2890
  }
2685
- console.log(`${logPrefix2} [%s] inferring headers from first data row`, outputFileName);
2891
+ logger.info(`${logPrefix2} [%s] inferring headers from first data row`, outputFileName);
2686
2892
  const headers = Object.keys(firstDataRow).map((col) => {
2687
2893
  return {
2688
2894
  id: col,
@@ -2692,14 +2898,14 @@ var writeGeneratorToCSV = async (generator, outputFileName) => {
2692
2898
  const csvWriter = createObjectCsvWriter({
2693
2899
  path: outputFileName,
2694
2900
  header: headers,
2695
- alwaysQuote: true
2901
+ alwaysQuote
2696
2902
  });
2697
- console.log(`${logPrefix2} [%s] writing csv`, outputFileName);
2903
+ logger.info(`${logPrefix2} [%s] writing csv`, outputFileName);
2698
2904
  let batch = [firstDataRow];
2699
2905
  for await (const data of generator) {
2700
2906
  batch.push(data);
2701
2907
  rowCount++;
2702
- if (batch.length > 1e4) {
2908
+ if (batch.length >= batchSize) {
2703
2909
  try {
2704
2910
  await csvWriter.writeRecords(batch);
2705
2911
  } catch (err) {
@@ -2717,8 +2923,10 @@ var writeGeneratorToCSV = async (generator, outputFileName) => {
2717
2923
  batch = [];
2718
2924
  }
2719
2925
  }
2720
- await csvWriter.writeRecords(batch);
2721
- console.log(`${logPrefix2} [%s] csv written successfully with %d rows`, outputFileName, rowCount);
2926
+ if (batch.length > 0) {
2927
+ await csvWriter.writeRecords(batch);
2928
+ }
2929
+ logger.info(`${logPrefix2} [%s] csv written successfully with %d rows`, outputFileName, rowCount);
2722
2930
  return rowCount;
2723
2931
  } catch (err) {
2724
2932
  if (err instanceof Error) {
@@ -3004,9 +3212,65 @@ var FilePatterns = class {
3004
3212
  };
3005
3213
 
3006
3214
  // src/services/sftp.ts
3215
+ import path4 from "path";
3216
+ import fs5 from "fs-extra";
3007
3217
  import Client from "ssh2-sftp-client";
3008
3218
  import { default as default2 } from "ssh2-sftp-client";
3009
3219
  var SftpService = new Client();
3220
+ var logPrefix3 = "[sftp]";
3221
+ async function listFilesRecursively(client, remotePath, options) {
3222
+ const recursive = options?.recursive ?? true;
3223
+ const extensions = options?.extensions?.map((e) => e.toLowerCase());
3224
+ const maxIterations = options?.maxIterations ?? 1e3;
3225
+ const results = [];
3226
+ const dirs = [remotePath];
3227
+ let iterations = 0;
3228
+ while (dirs.length > 0) {
3229
+ if (iterations++ >= maxIterations) {
3230
+ logger.warn(`${logPrefix3} listFilesRecursively reached maxIterations (${maxIterations}), stopping.`);
3231
+ break;
3232
+ }
3233
+ const currentDir = dirs.shift();
3234
+ let entries;
3235
+ try {
3236
+ entries = await client.list(currentDir);
3237
+ } catch (err) {
3238
+ const msg = err instanceof Error ? err.message : String(err);
3239
+ logger.warn(`${logPrefix3} Failed to list ${currentDir}: ${msg}`);
3240
+ continue;
3241
+ }
3242
+ for (const entry of entries) {
3243
+ if (entry.type === "d") {
3244
+ if (recursive && entry.name !== "." && entry.name !== "..") {
3245
+ dirs.push(path4.posix.join(currentDir, entry.name));
3246
+ }
3247
+ continue;
3248
+ }
3249
+ if (entry.type !== "-") continue;
3250
+ if (extensions) {
3251
+ const ext = path4.extname(entry.name).toLowerCase();
3252
+ if (!extensions.includes(ext)) continue;
3253
+ }
3254
+ const fullPath = path4.posix.join(currentDir, entry.name);
3255
+ results.push({ ...entry, name: fullPath });
3256
+ }
3257
+ }
3258
+ logger.info(`${logPrefix3} listFilesRecursively found ${results.length} files under ${remotePath}`);
3259
+ return results;
3260
+ }
3261
+ async function downloadFiles(client, files, localDir, options) {
3262
+ const concurrency = options?.concurrency ?? 5;
3263
+ await fs5.ensureDir(localDir);
3264
+ const tasks = files.map((file, index) => () => {
3265
+ const localName = `${index}_${path4.basename(file.name)}`;
3266
+ const localPath = path4.join(localDir, localName);
3267
+ logger.debug(`${logPrefix3} Downloading ${file.name} \u2192 ${localPath}`);
3268
+ return client.fastGet(file.name, localPath).then(() => localPath);
3269
+ });
3270
+ const results = await runWithConcurrency(tasks, concurrency);
3271
+ logger.info(`${logPrefix3} Downloaded ${results.length} files to ${localDir}`);
3272
+ return results;
3273
+ }
3010
3274
 
3011
3275
  // src/services/cloud-storage.ts
3012
3276
  import { Storage } from "@google-cloud/storage";
@@ -3014,7 +3278,7 @@ import { format as format6 } from "util";
3014
3278
  import * as pathModule from "path";
3015
3279
  import { existsSync, mkdirSync, unlinkSync as unlinkSync2 } from "fs";
3016
3280
  import { randomUUID } from "crypto";
3017
- var logPrefix3 = "[CloudStorageService]";
3281
+ var logPrefix4 = "[CloudStorageService]";
3018
3282
  var tmpDir = "tmp";
3019
3283
  var CloudStorageService = class {
3020
3284
  storage;
@@ -3022,10 +3286,10 @@ var CloudStorageService = class {
3022
3286
  processedSuffix;
3023
3287
  supportedExtensions;
3024
3288
  path;
3025
- constructor(bucketName, path2, opts) {
3289
+ constructor(bucketName, path7, opts) {
3026
3290
  this.storage = new Storage({ retryOptions: { autoRetry: true, maxRetries: 20 } });
3027
3291
  this.bucket = this.storage.bucket(bucketName);
3028
- this.path = path2;
3292
+ this.path = path7;
3029
3293
  this.processedSuffix = opts?.processedSuffix ?? ".processed";
3030
3294
  this.supportedExtensions = opts?.supportedExtensions ?? [];
3031
3295
  }
@@ -3066,7 +3330,7 @@ var CloudStorageService = class {
3066
3330
  try {
3067
3331
  const file = this.bucket.file(markerFileName);
3068
3332
  await file.save(format6("Marked file %s as processed", fileName));
3069
- logger.info(`${logPrefix3} Marker file ${markerFileName} created successfully.`);
3333
+ logger.info(`${logPrefix4} Marker file ${markerFileName} created successfully.`);
3070
3334
  } catch (err) {
3071
3335
  if (err instanceof Error) {
3072
3336
  throw new Error(format6(`error while writing marker file=%s, err:%s`, markerFileName, err.message));
@@ -3090,7 +3354,7 @@ var CloudStorageService = class {
3090
3354
  unlinkSync2(destFilename);
3091
3355
  }
3092
3356
  await file.download({ destination: destFilename });
3093
- logger.info(`${logPrefix3} Downloaded ${fileName} to ${destFilename}`);
3357
+ logger.info(`${logPrefix4} Downloaded ${fileName} to ${destFilename}`);
3094
3358
  return destFilename;
3095
3359
  } catch (err) {
3096
3360
  if (err instanceof Error) {
@@ -3108,9 +3372,9 @@ var CloudStorageService = class {
3108
3372
  */
3109
3373
  async uploadFile(localPath, destPath) {
3110
3374
  try {
3111
- logger.info(`${logPrefix3} preparing to upload '%s' to '%s'`, localPath, destPath);
3375
+ logger.info(`${logPrefix4} preparing to upload '%s' to '%s'`, localPath, destPath);
3112
3376
  const [file] = await this.bucket.upload(localPath, { destination: destPath });
3113
- logger.info(`${logPrefix3} file %s has been successfully uploaded into %s`, localPath, destPath);
3377
+ logger.info(`${logPrefix4} file %s has been successfully uploaded into %s`, localPath, destPath);
3114
3378
  return file;
3115
3379
  } catch (err) {
3116
3380
  if (err instanceof Error) {
@@ -3186,6 +3450,376 @@ var unzip = async (zipFilePath, extractedPath) => {
3186
3450
  return extractedPath;
3187
3451
  };
3188
3452
 
3453
+ // src/services/cdn.ts
3454
+ import axios3 from "axios";
3455
+ import axiosRetry2 from "axios-retry";
3456
+ var logPrefix5 = "[CdnService]";
3457
+ var MAX_RETRIES = 10;
3458
+ var CdnService = class {
3459
+ axiosClient;
3460
+ constructor(config) {
3461
+ const resolvedKey = getServiceAccountKey(config.serviceAccountKey);
3462
+ const resolvedEndpoint = getApiEndpoint(config.apiEndpoint);
3463
+ this.axiosClient = axios3.create({
3464
+ baseURL: resolvedEndpoint,
3465
+ timeout: 12e4,
3466
+ headers: {
3467
+ Authorization: `Bearer ${resolvedKey}`,
3468
+ Accept: "application/json"
3469
+ }
3470
+ });
3471
+ axiosRetry2(this.axiosClient, {
3472
+ retries: MAX_RETRIES,
3473
+ retryCondition: (error) => {
3474
+ const status = error.response?.status ?? 0;
3475
+ return !error.response || status === 429 || status >= 500 && status < 600;
3476
+ },
3477
+ retryDelay: (retryCount, error) => {
3478
+ const retryAfter = error.response?.headers?.["retry-after"];
3479
+ if (retryAfter) {
3480
+ const seconds = Number(retryAfter);
3481
+ if (!isNaN(seconds) && seconds > 0) {
3482
+ logger.info(`${logPrefix5} Rate limited \u2014 waiting ${seconds}s (Retry-After header), attempt ${retryCount}/${MAX_RETRIES}`);
3483
+ return seconds * 1e3;
3484
+ }
3485
+ }
3486
+ const delay = axiosRetry2.exponentialDelay(retryCount);
3487
+ logger.info(`${logPrefix5} Retrying in ${delay}ms (attempt ${retryCount}/${MAX_RETRIES}) for ${error.config?.method?.toUpperCase()} ${error.config?.url}`);
3488
+ return delay;
3489
+ }
3490
+ });
3491
+ }
3492
+ /**
3493
+ * Returns metadata for a CDN file via a HEAD request.
3494
+ * If the file does not exist, returns `{ exists: false, lastModified: null, contentType: null }`.
3495
+ * Falls back to not-existing if HEAD is not supported (405).
3496
+ */
3497
+ async getFileMetadata(cdnId, fileName) {
3498
+ const url = `/v1/cdn/${cdnId}/files/${encodeURIComponent(fileName)}`;
3499
+ try {
3500
+ const response = await this.axiosClient.head(url);
3501
+ const lastModifiedHeader = response.headers["last-modified"];
3502
+ const contentTypeHeader = response.headers["content-type"];
3503
+ return {
3504
+ exists: true,
3505
+ lastModified: lastModifiedHeader ? new Date(lastModifiedHeader) : null,
3506
+ contentType: contentTypeHeader ?? null
3507
+ };
3508
+ } catch (err) {
3509
+ if (axios3.isAxiosError(err)) {
3510
+ const status = err.response?.status;
3511
+ if (status === 404) return { exists: false, lastModified: null, contentType: null };
3512
+ if (status === 405) {
3513
+ logger.warn(`${logPrefix5} HEAD not supported for ${url}, treating file as absent`);
3514
+ return { exists: false, lastModified: null, contentType: null };
3515
+ }
3516
+ this.throwMeaningfulError(err);
3517
+ }
3518
+ throw err;
3519
+ }
3520
+ }
3521
+ /**
3522
+ * Checks whether a file already exists in the CDN via a HEAD request.
3523
+ * Returns true if the server responds 2xx, false on 404.
3524
+ * Falls back to false (treat as not existing) if HEAD is not supported (405).
3525
+ * No body is transferred.
3526
+ */
3527
+ async fileExists(cdnId, fileName) {
3528
+ return (await this.getFileMetadata(cdnId, fileName)).exists;
3529
+ }
3530
+ /**
3531
+ * Uploads a file buffer to the Whaly CDN.
3532
+ * The PUT is idempotent — uploading the same fileName overwrites the existing file.
3533
+ */
3534
+ async uploadFile(cdnId, fileName, fileBuffer) {
3535
+ const url = `/v1/cdn/${cdnId}/files/${encodeURIComponent(fileName)}`;
3536
+ try {
3537
+ logger.info(`${logPrefix5} Uploading ${fileName} (${fileBuffer.length} bytes) to CDN ${cdnId}`);
3538
+ const form = new FormData();
3539
+ form.append("file", new Blob([fileBuffer]), fileName);
3540
+ const response = await this.axiosClient.put(url, form);
3541
+ logger.info(`${logPrefix5} Successfully uploaded ${fileName} (status ${response.status})`);
3542
+ return {
3543
+ filePath: response.data?.filePath ?? `/org/${cdnId}/file/${fileName}`
3544
+ };
3545
+ } catch (err) {
3546
+ if (axios3.isAxiosError(err)) {
3547
+ this.throwMeaningfulError(err);
3548
+ }
3549
+ throw err;
3550
+ }
3551
+ }
3552
+ throwMeaningfulError(err) {
3553
+ const status = err.response?.status;
3554
+ if (status === 401 || status === 403) {
3555
+ throw new Error("Unauthorized - invalid or expired service account key");
3556
+ } else if (status === 404) {
3557
+ throw new Error("Organization or CDN not found");
3558
+ } else if (status === 500) {
3559
+ throw new Error("API server error - check CDN ID and service account permissions");
3560
+ } else {
3561
+ throw new Error(`API error: ${err.response?.statusText ?? err.message}`);
3562
+ }
3563
+ }
3564
+ };
3565
+
3566
+ // src/sdk/models/asset-tap/asset-stream.ts
3567
+ var AssetStream = class {
3568
+ streamId;
3569
+ config;
3570
+ replicationMode;
3571
+ constructor(streamId, config, replicationMode = "INCREMENTAL") {
3572
+ this.streamId = streamId;
3573
+ this.config = config;
3574
+ this.replicationMode = replicationMode;
3575
+ }
3576
+ async transformFile(downloadedPath, _entry) {
3577
+ return downloadedPath;
3578
+ }
3579
+ };
3580
+
3581
+ // src/sdk/models/asset-tap/asset-tap.ts
3582
+ import fs6 from "fs-extra";
3583
+ import path5 from "path";
3584
+ var logPrefix6 = "[AssetTap]";
3585
+ function inferContentType(filePath, fallback) {
3586
+ const mime = getMimeType(filePath);
3587
+ return mime !== "application/octet-stream" ? mime : fallback;
3588
+ }
3589
+ function deriveManifestMode(streams) {
3590
+ const modes = new Set(streams.map((s) => s.replicationMode));
3591
+ if (modes.size > 1) {
3592
+ logger.warn(`${logPrefix6} Streams have mixed replication modes (${[...modes].join(", ")}). Manifest will report "FULL".`);
3593
+ return "FULL";
3594
+ }
3595
+ return streams[0]?.replicationMode ?? "INCREMENTAL";
3596
+ }
3597
+ var AssetTap = class {
3598
+ config;
3599
+ outputDir;
3600
+ streams = [];
3601
+ concurrency;
3602
+ target;
3603
+ constructor(target, config, outputDir = "out", concurrency = 5) {
3604
+ this.target = target;
3605
+ this.config = config;
3606
+ this.outputDir = outputDir;
3607
+ this.concurrency = concurrency;
3608
+ }
3609
+ async sync() {
3610
+ await this.init();
3611
+ const dryRun = isDryRun();
3612
+ const dryRunLimit = dryRun ? getDryRunLimit() : void 0;
3613
+ if (dryRun) {
3614
+ logger.info(`${logPrefix6} [DRY_RUN] mode active \u2014 skipping CDN checks and uploads`);
3615
+ await fs6.emptyDir(this.outputDir);
3616
+ if (dryRunLimit !== void 0) {
3617
+ logger.info(`${logPrefix6} [DRY_RUN] Limit: ${dryRunLimit} assets per stream`);
3618
+ }
3619
+ } else if (process.env["DRY_RUN_LIMIT"] !== void 0) {
3620
+ logger.warn(`${logPrefix6} DRY_RUN_LIMIT is set but DRY_RUN is not active \u2014 limit will be ignored`);
3621
+ }
3622
+ const tmpDir2 = path5.join(this.outputDir, "tmp");
3623
+ await fs6.ensureDir(tmpDir2);
3624
+ const streamManifests = [];
3625
+ let entryIndex = 0;
3626
+ let totalSummary = { total: 0, uploaded: 0, skipped: 0, errors: 0 };
3627
+ for (const stream of this.streams) {
3628
+ logger.info(`${logPrefix6} Processing stream: ${stream.streamId} (mode=${stream.replicationMode}, concurrency=${this.concurrency})`);
3629
+ const assetEntries = [];
3630
+ let streamAssetCount = 0;
3631
+ await processFromAsyncIterable(
3632
+ stream.listAssets(),
3633
+ async (entry) => {
3634
+ if (dryRunLimit !== void 0 && streamAssetCount >= dryRunLimit) {
3635
+ return "stop";
3636
+ }
3637
+ logger.debug(`${logPrefix6} Processing entry: ${entry.sourcePath}`);
3638
+ if (stream.replicationMode === "INCREMENTAL" && !dryRun) {
3639
+ const shouldSync = await this.target.shouldSync(entry);
3640
+ if (!shouldSync) {
3641
+ logger.debug(`${logPrefix6} Skipping ${entry.sourcePath} (up-to-date)`);
3642
+ assetEntries.push({
3643
+ sourcePath: entry.sourcePath,
3644
+ destinationPath: entry.destinationPath,
3645
+ downloadedPath: "",
3646
+ transformedPath: "",
3647
+ size: 0,
3648
+ contentType: entry.contentType,
3649
+ status: "skipped",
3650
+ transformed: false
3651
+ });
3652
+ return "continue";
3653
+ }
3654
+ }
3655
+ const fileName = `${entryIndex}_${path5.basename(entry.sourcePath)}`;
3656
+ const downloadedPath = path5.join(tmpDir2, fileName);
3657
+ let uploadPath = downloadedPath;
3658
+ entryIndex++;
3659
+ streamAssetCount++;
3660
+ try {
3661
+ await stream.downloadEntry(entry, downloadedPath);
3662
+ uploadPath = await stream.transformFile(downloadedPath, entry);
3663
+ const wasTransformed = uploadPath !== downloadedPath;
3664
+ const stat = await fs6.stat(uploadPath);
3665
+ const processed = {
3666
+ entry,
3667
+ downloadedPath,
3668
+ uploadPath,
3669
+ wasTransformed,
3670
+ size: stat.size,
3671
+ contentType: wasTransformed ? inferContentType(uploadPath, entry.contentType) : entry.contentType
3672
+ };
3673
+ if (!dryRun) {
3674
+ await this.target.uploadAsset(processed);
3675
+ } else {
3676
+ const inspectPath = path5.join(this.outputDir, stream.streamId, entry.destinationPath);
3677
+ await fs6.ensureDir(path5.dirname(inspectPath));
3678
+ await fs6.copy(uploadPath, inspectPath);
3679
+ }
3680
+ assetEntries.push({
3681
+ sourcePath: entry.sourcePath,
3682
+ destinationPath: entry.destinationPath,
3683
+ downloadedPath,
3684
+ transformedPath: wasTransformed ? uploadPath : "",
3685
+ size: processed.size,
3686
+ contentType: processed.contentType,
3687
+ status: "uploaded",
3688
+ transformed: wasTransformed
3689
+ });
3690
+ logger.info(`${logPrefix6} ${dryRun ? "[DRY_RUN] Processed" : "Uploaded"} ${entry.sourcePath} \u2192 ${entry.destinationPath}`);
3691
+ } catch (err) {
3692
+ const message = err instanceof Error ? err.message : String(err);
3693
+ logger.error(`${logPrefix6} Failed to process ${entry.sourcePath}: ${message}`);
3694
+ assetEntries.push({
3695
+ sourcePath: entry.sourcePath,
3696
+ destinationPath: entry.destinationPath,
3697
+ downloadedPath: "",
3698
+ transformedPath: "",
3699
+ size: 0,
3700
+ contentType: entry.contentType,
3701
+ status: "error",
3702
+ transformed: false,
3703
+ error: message
3704
+ });
3705
+ } finally {
3706
+ await fs6.remove(downloadedPath).catch(() => void 0);
3707
+ if (uploadPath !== downloadedPath) {
3708
+ await fs6.remove(uploadPath).catch(() => void 0);
3709
+ }
3710
+ }
3711
+ if (dryRunLimit !== void 0 && streamAssetCount >= dryRunLimit) {
3712
+ logger.info(`${logPrefix6} [DRY_RUN] Reached limit of ${dryRunLimit} for stream "${stream.streamId}", stopping.`);
3713
+ return "stop";
3714
+ }
3715
+ return "continue";
3716
+ },
3717
+ this.concurrency
3718
+ );
3719
+ const streamSummary = {
3720
+ total: assetEntries.length,
3721
+ uploaded: assetEntries.filter((a) => a.status === "uploaded").length,
3722
+ skipped: assetEntries.filter((a) => a.status === "skipped").length,
3723
+ errors: assetEntries.filter((a) => a.status === "error").length
3724
+ };
3725
+ totalSummary.total += streamSummary.total;
3726
+ totalSummary.uploaded += streamSummary.uploaded;
3727
+ totalSummary.skipped += streamSummary.skipped;
3728
+ totalSummary.errors += streamSummary.errors;
3729
+ streamManifests.push({
3730
+ streamId: stream.streamId,
3731
+ mode: stream.replicationMode,
3732
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
3733
+ assets: assetEntries,
3734
+ summary: streamSummary
3735
+ });
3736
+ }
3737
+ const manifest = {
3738
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
3739
+ mode: deriveManifestMode(this.streams),
3740
+ streams: streamManifests,
3741
+ summary: totalSummary
3742
+ };
3743
+ await fs6.ensureDir(this.outputDir);
3744
+ await fs6.writeJson(path5.join(this.outputDir, "manifest.json"), manifest, { spaces: 2 });
3745
+ if (!dryRun) {
3746
+ await this.target.complete();
3747
+ }
3748
+ logger.info(`${logPrefix6} Sync complete. Uploaded=${totalSummary.uploaded} Skipped=${totalSummary.skipped} Errors=${totalSummary.errors}`);
3749
+ return manifest;
3750
+ }
3751
+ };
3752
+
3753
+ // src/sdk/models/asset-tap/image-transform.ts
3754
+ import sharp from "sharp";
3755
+ import path6 from "path";
3756
+ var SHARP_GRAVITY = {
3757
+ center: "centre",
3758
+ top: "north",
3759
+ bottom: "south",
3760
+ left: "west",
3761
+ right: "east",
3762
+ "top left": "northwest",
3763
+ "top right": "northeast",
3764
+ "bottom left": "southwest",
3765
+ "bottom right": "southeast"
3766
+ };
3767
+ function parseBackground(bg) {
3768
+ if (bg === void 0 || bg === "none" || bg === "transparent") {
3769
+ return { r: 0, g: 0, b: 0, alpha: 0 };
3770
+ }
3771
+ return bg;
3772
+ }
3773
+ var ImageTransform = class {
3774
+ /**
3775
+ * Converts an image to WebP format using sharp (libvips).
3776
+ *
3777
+ * The output file is written to the same directory as the input,
3778
+ * with the extension replaced by `.webp`.
3779
+ *
3780
+ * @param inputPath Path to the source image file.
3781
+ * @param options Optional resize / padding parameters.
3782
+ * @returns Path to the produced `.webp` file.
3783
+ */
3784
+ static async toWebp(inputPath, options) {
3785
+ const dir = path6.dirname(inputPath);
3786
+ const basename = path6.basename(inputPath, path6.extname(inputPath));
3787
+ const outputPath = path6.join(dir, `${basename}.webp`);
3788
+ let pipeline = sharp(inputPath);
3789
+ const hasSize = options.width !== void 0 && options.height !== void 0;
3790
+ if (hasSize) {
3791
+ const position = options.gravity !== void 0 ? SHARP_GRAVITY[options.gravity] ?? options.gravity : void 0;
3792
+ if (options.extent === true) {
3793
+ pipeline = pipeline.resize(options.width, options.height, {
3794
+ fit: "contain",
3795
+ position,
3796
+ background: parseBackground(options.background)
3797
+ });
3798
+ } else {
3799
+ pipeline = pipeline.resize(options.width, options.height, {
3800
+ fit: "inside",
3801
+ position
3802
+ });
3803
+ }
3804
+ }
3805
+ pipeline = pipeline.webp(
3806
+ options.quality !== void 0 ? { quality: options.quality } : {}
3807
+ );
3808
+ await pipeline.toFile(outputPath);
3809
+ return outputPath;
3810
+ }
3811
+ };
3812
+
3813
+ // src/sdk/models/asset-target/asset-target.ts
3814
+ var AssetTarget = class {
3815
+ config;
3816
+ constructor(config) {
3817
+ this.config = config;
3818
+ }
3819
+ async complete() {
3820
+ }
3821
+ };
3822
+
3189
3823
  // src/targets/bigquery/helpers.ts
3190
3824
  var safeColumnName = (key) => {
3191
3825
  let returnString = key;
@@ -3222,7 +3856,9 @@ import Decimal from "decimal.js";
3222
3856
  import dayjs5 from "dayjs";
3223
3857
  var validateDateRange = (record, schema) => {
3224
3858
  Object.keys(schema).forEach((key) => {
3225
- const { format: format8 } = schema[key];
3859
+ const field = schema[key];
3860
+ if (!field) return;
3861
+ const { format: format8 } = field;
3226
3862
  if (format8 === "date-time") {
3227
3863
  if (record[key]) {
3228
3864
  const formattedDate = dayjs5(record[key]).format(defaultDateTimeFormat);
@@ -3452,6 +4088,12 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
3452
4088
  });
3453
4089
  };
3454
4090
  getReplaceQueries = () => {
4091
+ if (this.primaryKeys.length === 0) {
4092
+ return [
4093
+ `TRUNCATE TABLE ${this.sqlTableId};`,
4094
+ ...this.getAppendQueries()
4095
+ ];
4096
+ }
3455
4097
  return [
3456
4098
  `TRUNCATE TABLE ${this.sqlTableId};`,
3457
4099
  ...this.getMergeQueries()
@@ -3739,6 +4381,46 @@ var BigQueryTarget = class extends ITarget {
3739
4381
  convertNumberIntoDecimal = convertNumberIntoDecimal;
3740
4382
  };
3741
4383
 
4384
+ // src/targets/cdn/main.ts
4385
+ import fs7 from "fs-extra";
4386
+ var logPrefix7 = "[CdnAssetTarget]";
4387
+ var CdnAssetTarget = class extends AssetTarget {
4388
+ cdnService;
4389
+ constructor(config) {
4390
+ super(config);
4391
+ this.cdnService = new CdnService(config);
4392
+ }
4393
+ async shouldSync(entry) {
4394
+ const metadata = await this.cdnService.getFileMetadata(getCdnId(this.config.cdnId), entry.destinationPath);
4395
+ if (!metadata.exists) {
4396
+ logger.debug(`${logPrefix7} ${entry.destinationPath} not in CDN \u2192 will sync`);
4397
+ return true;
4398
+ }
4399
+ if (entry.lastModified === void 0) {
4400
+ logger.debug(`${logPrefix7} ${entry.destinationPath} source has no lastModified \u2192 will sync`);
4401
+ return true;
4402
+ }
4403
+ if (metadata.lastModified === null) {
4404
+ logger.debug(`${logPrefix7} ${entry.destinationPath} CDN has no lastModified \u2192 will sync`);
4405
+ return true;
4406
+ }
4407
+ const sourceIsNewer = entry.lastModified > metadata.lastModified;
4408
+ if (!sourceIsNewer) {
4409
+ logger.debug(`${logPrefix7} Skipping ${entry.destinationPath} (CDN is up-to-date)`);
4410
+ }
4411
+ return sourceIsNewer;
4412
+ }
4413
+ async uploadAsset(asset) {
4414
+ const fileBuffer = await fs7.readFile(asset.uploadPath);
4415
+ await this.cdnService.uploadFile(
4416
+ getCdnId(this.config.cdnId),
4417
+ asset.entry.destinationPath,
4418
+ fileBuffer
4419
+ );
4420
+ logger.info(`${logPrefix7} Uploaded ${asset.entry.destinationPath} (${fileBuffer.length} bytes)`);
4421
+ }
4422
+ };
4423
+
3742
4424
  // src/state-providers/gcs/main.ts
3743
4425
  import { format as format7 } from "util";
3744
4426
  var GCSStateProvider = class {
@@ -3779,9 +4461,14 @@ var GCSStateProvider = class {
3779
4461
  export {
3780
4462
  ALL_STREAM_ID_KEYWORD,
3781
4463
  API_CALLS_METRIC_NAME,
4464
+ AssetStream,
4465
+ AssetTap,
4466
+ AssetTarget,
3782
4467
  Authenticator,
3783
4468
  BATCH_INTERVAL_MS,
3784
4469
  BigQueryTarget,
4470
+ CdnAssetTarget,
4471
+ CdnService,
3785
4472
  CloudStorageService,
3786
4473
  CounterMetric,
3787
4474
  CsvExtractionConfigBuilder,
@@ -3795,6 +4482,8 @@ export {
3795
4482
  FileTap,
3796
4483
  GCSStateProvider,
3797
4484
  ITarget,
4485
+ ImageTransform,
4486
+ MIME_TYPES,
3798
4487
  MissingFieldInSchemaError,
3799
4488
  MissingSchemaError,
3800
4489
  RESTStream,
@@ -3816,6 +4505,7 @@ export {
3816
4505
  _greaterThan,
3817
4506
  addWhalyFields,
3818
4507
  checkCsvHeaderRow,
4508
+ collectPaginated,
3819
4509
  countCsvLines,
3820
4510
  createCellReference,
3821
4511
  createCsvStreamConfig,
@@ -3823,6 +4513,7 @@ export {
3823
4513
  createExcelStreamConfig,
3824
4514
  createTemporaryFileStream,
3825
4515
  csvFieldsToJsonSchema,
4516
+ downloadFiles,
3826
4517
  excelColumnToIndex,
3827
4518
  excelFieldsToJsonSchema,
3828
4519
  excelRowToIndex,
@@ -3835,29 +4526,40 @@ export {
3835
4526
  findAllMatchingConfigs,
3836
4527
  flattenSchema,
3837
4528
  getAllMetrics,
4529
+ getApiEndpoint,
3838
4530
  getAxiosInstance,
4531
+ getCdnId,
3839
4532
  getCounterMetric,
3840
4533
  getCounterMetrics,
3841
4534
  getDownloadFileApiCall,
4535
+ getDryRunLimit,
3842
4536
  getJSONApiCall,
3843
4537
  getJSONApiCallWithFullResponse,
4538
+ getMimeType,
3844
4539
  getRawJSONApiCall,
4540
+ getServiceAccountKey,
3845
4541
  gracefulExit,
3846
4542
  haltAndCatchFire,
3847
4543
  incrementStreamState,
3848
4544
  indexToExcelColumn,
3849
4545
  indexToExcelRow,
4546
+ isDryRun,
4547
+ listFilesRecursively,
3850
4548
  loadJson,
3851
4549
  loadSchema,
3852
4550
  logger,
4551
+ optionalEnv,
3853
4552
  parseCellReference,
3854
4553
  postFormDataApiCall,
3855
4554
  postJSONApiCall,
3856
4555
  postUrlEncodedApiCall,
3857
4556
  printMemoryFootprint,
3858
4557
  processFileStreams,
4558
+ processFromAsyncIterable,
3859
4559
  removeParasiteProperties,
4560
+ requireEnv,
3860
4561
  rowGeneratorFromCsv,
4562
+ runWithConcurrency,
3861
4563
  safePath,
3862
4564
  unzip,
3863
4565
  validateExcelConfig,