@whaly/connector-sdk 0.3.3 → 0.3.5

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);
@@ -1698,15 +1865,15 @@ import fs2 from "fs-extra";
1698
1865
  import uniqueId from "lodash/uniqueId.js";
1699
1866
  var createTemporaryFileStream = (streamId) => {
1700
1867
  fs2.ensureDirSync("./tmp");
1701
- const path2 = `./tmp/${safePath(streamId)}-${uniqueId()}.tmp`;
1702
- const writeStream = fs2.createWriteStream(path2, { flags: "w" });
1868
+ const path7 = `./tmp/${safePath(streamId)}-${uniqueId()}.tmp`;
1869
+ const writeStream = fs2.createWriteStream(path7, { flags: "w" });
1703
1870
  return {
1704
1871
  stream: writeStream,
1705
- path: path2
1872
+ path: path7
1706
1873
  };
1707
1874
  };
1708
- function safePath(path2) {
1709
- let formattedPath = path2;
1875
+ function safePath(path7) {
1876
+ let formattedPath = path7;
1710
1877
  formattedPath = formattedPath.replace(/^The\s+/gi, "");
1711
1878
  formattedPath = formattedPath.replace(/\s+(?:\(|\[)?Dis(?:c|k) (?:One|Two|Three|Four|Five|Six|Seven|Eight|Nine|Ten|\d)+(?:\)|\])?/gi, "");
1712
1879
  formattedPath = formattedPath.replace(/\s+/gi, "_");
@@ -1832,10 +1999,13 @@ var StreamState = class {
1832
1999
  // src/sdk/models/target/target.ts
1833
2000
  import { Semaphore } from "async-mutex";
1834
2001
  import Bluebird3 from "bluebird";
2002
+ import fs4 from "fs-extra";
2003
+ import path2 from "path";
1835
2004
  var semaphore = new Semaphore(1);
1836
2005
  var ITarget = class _ITarget {
1837
2006
  config;
1838
2007
  syncedAtColumnName;
2008
+ syncedAtColumnUseLegacyStringType;
1839
2009
  schemaHooks;
1840
2010
  syncTime;
1841
2011
  stateProvider;
@@ -1852,6 +2022,7 @@ var ITarget = class _ITarget {
1852
2022
  constructor(config, stateProvider) {
1853
2023
  this.config = config;
1854
2024
  this.syncedAtColumnName = config.syncedAtColumnName ?? DEFAULT_SYNCED_AT_COLUMN;
2025
+ this.syncedAtColumnUseLegacyStringType = config.syncedAtColumnUseLegacyStringType ?? false;
1855
2026
  this.stateProvider = stateProvider;
1856
2027
  this.schemaHooks = [];
1857
2028
  this.streams = {};
@@ -1916,7 +2087,7 @@ var ITarget = class _ITarget {
1916
2087
  if (!stream) {
1917
2088
  throw new MissingSchemaError(`A record for stream:\`${streamId}\` was encountered before a corresponding schema.`, streamId);
1918
2089
  }
1919
- if (stream.getBatchedRowCount() % 1e4 === 0) {
2090
+ if (stream.getBatchedRowCount() % 1e4 === 0 && !isDryRun()) {
1920
2091
  await semaphore.runExclusive(async () => {
1921
2092
  const shouldUploadIntermediateBatch = this.shouldUploadIntermediateBatch(streamId);
1922
2093
  if (shouldUploadIntermediateBatch) {
@@ -2002,7 +2173,9 @@ var ITarget = class _ITarget {
2002
2173
  const { hasBreakingChanges, changes } = this.analyzeSchemaChanges(prevSchema, newFlattenedSchema, streamId);
2003
2174
  if (hasBreakingChanges) {
2004
2175
  logger.info(`\u{1F525} StreamId=${streamId} - Detected BREAKING schema changes: ${changes.join(", ")}. Loading batched records before applying new schema.`);
2005
- await _ITarget.uploadSingleStreamToWarehouse(prevStreamState);
2176
+ if (!isDryRun()) {
2177
+ await _ITarget.uploadSingleStreamToWarehouse(prevStreamState);
2178
+ }
2006
2179
  } else {
2007
2180
  logger.info(`\u2705 StreamId=${streamId} - Detected NON-BREAKING schema changes: ${changes.join(", ")}. Continuing without uploading batched records.`);
2008
2181
  }
@@ -2014,10 +2187,7 @@ var ITarget = class _ITarget {
2014
2187
  throw new MissingSchemaError(`Stream: ${streamId} - Received SCHEMA message before stream state was initialized.`, streamId);
2015
2188
  }
2016
2189
  const schema = flattenSchema(streamId, message.schema);
2017
- schema[this.syncedAtColumnName] = {
2018
- format: "date-time",
2019
- type: ["null", "string"]
2020
- };
2190
+ schema[this.syncedAtColumnName] = this.syncedAtColumnUseLegacyStringType ? { type: ["null", "string"] } : { format: "date-time", type: ["null", "string"] };
2021
2191
  const dbSync = streamState.getDbSync();
2022
2192
  Object.keys(schema).forEach((fieldUnsafeKey) => {
2023
2193
  const fieldDef = schema[fieldUnsafeKey];
@@ -2034,31 +2204,35 @@ var ITarget = class _ITarget {
2034
2204
  Received SCHEMA message: ${JSON.stringify(message)}`);
2035
2205
  }
2036
2206
  streamState.setKeyProperties(message.keyProperties);
2037
- await dbSync.updateSchemaInWarehouse(schema, message.keyProperties);
2038
- const {
2039
- database: databaseName,
2040
- schema: schemaName
2041
- } = this.config;
2042
- const tableName = this.genTableName(streamId);
2043
- const formattedRelationshipMessage = {
2044
- ...message,
2045
- keyProperties: message.keyProperties.map((k) => {
2046
- const translation = streamState.getDbSync().renamedColumnStore.getColumnTranslation(message.stream, k);
2047
- if (translation) {
2048
- return translation;
2049
- }
2050
- return k;
2051
- })
2052
- };
2053
- await Bluebird3.map(this.schemaHooks, async (hook) => {
2054
- const input = {
2055
- databaseName,
2056
- schemaName,
2057
- tableName,
2058
- message: formattedRelationshipMessage
2207
+ if (!isDryRun()) {
2208
+ await dbSync.updateSchemaInWarehouse(schema, message.keyProperties);
2209
+ const {
2210
+ database: databaseName,
2211
+ schema: schemaName
2212
+ } = this.config;
2213
+ const tableName = this.genTableName(streamId);
2214
+ const formattedRelationshipMessage = {
2215
+ ...message,
2216
+ keyProperties: message.keyProperties.map((k) => {
2217
+ const translation = streamState.getDbSync().renamedColumnStore.getColumnTranslation(message.stream, k);
2218
+ if (translation) {
2219
+ return translation;
2220
+ }
2221
+ return k;
2222
+ })
2059
2223
  };
2060
- await hook.writeSchema(input);
2061
- }, { concurrency: 3 });
2224
+ await Bluebird3.map(this.schemaHooks, async (hook) => {
2225
+ const input = {
2226
+ databaseName,
2227
+ schemaName,
2228
+ tableName,
2229
+ message: formattedRelationshipMessage
2230
+ };
2231
+ await hook.writeSchema(input);
2232
+ }, { concurrency: 3 });
2233
+ } else {
2234
+ logger.info(`[DRY_RUN] Stream: ${streamId} - Skipping warehouse schema update and schema hooks`);
2235
+ }
2062
2236
  return Promise.resolve();
2063
2237
  } catch (err) {
2064
2238
  logger.error(`Error when handling schema event.`, err);
@@ -2079,6 +2253,29 @@ var ITarget = class _ITarget {
2079
2253
  };
2080
2254
  complete = async () => {
2081
2255
  try {
2256
+ if (isDryRun()) {
2257
+ logger.info(`[DRY_RUN] Data Extraction is complete. Copying tmp files to out/ instead of loading to warehouse.`);
2258
+ const outDir = path2.resolve("out");
2259
+ await fs4.ensureDir(outDir);
2260
+ for (const streamId of Object.keys(this.streams)) {
2261
+ const streamState = this.streams[streamId];
2262
+ if (!streamState || streamState.getBatchedRowCount() === 0) continue;
2263
+ const fileToLoad = streamState.getFileToLoad();
2264
+ await new Promise((resolve, reject) => {
2265
+ fileToLoad.stream.end(() => {
2266
+ fileToLoad.stream.close((err) => {
2267
+ if (err) reject(err);
2268
+ else resolve();
2269
+ });
2270
+ });
2271
+ });
2272
+ const destPath = path2.join(outDir, `${safePath(streamId)}.ndjson`);
2273
+ await fs4.copy(fileToLoad.path, destPath);
2274
+ logger.info(`[DRY_RUN] Stream: ${streamId} - Copied ${streamState.getBatchedRowCount()} records to ${destPath}`);
2275
+ await fs4.remove(fileToLoad.path);
2276
+ }
2277
+ return;
2278
+ }
2082
2279
  logger.info(`\u{1F44D} Data Extraction is complete. Will load remaining batched stream records.`);
2083
2280
  await this.loadAllStreamsInWarehouse({ isFinalLoad: true });
2084
2281
  return Promise.resolve();
@@ -2217,7 +2414,7 @@ function extractPrimaryKeysFromExcelFields(fields) {
2217
2414
 
2218
2415
  // src/sdk/file-processing/excel/reader.ts
2219
2416
  import XLSX from "xlsx";
2220
- import path from "path";
2417
+ import path3 from "path";
2221
2418
  var excelColumnToIndex = (columnLetter) => {
2222
2419
  const letters = columnLetter.toUpperCase();
2223
2420
  let index = 0;
@@ -2376,7 +2573,7 @@ var validateExcelConfig = (config) => {
2376
2573
  }
2377
2574
  };
2378
2575
  var findAllMatchingConfigs = (filename, configs) => {
2379
- const fileInfo = path.parse(filename);
2576
+ const fileInfo = path3.parse(filename);
2380
2577
  return configs.filter(
2381
2578
  (config) => config.fileNameValidator(fileInfo.base) && config.extension === fileInfo.ext.replace(".", "")
2382
2579
  );
@@ -2511,7 +2708,7 @@ var formatHexComparison = (expected, actual) => {
2511
2708
  Expected: "${expected}" (${expectedInfo.length} chars) [${expectedInfo.hex}]
2512
2709
  Actual: "${actual}" (${actualInfo.length} chars) [${actualInfo.hex}]`;
2513
2710
  };
2514
- async function* rowGeneratorFromCsv(path2, fileConfig) {
2711
+ async function* rowGeneratorFromCsv(path7, fileConfig) {
2515
2712
  const isGeneratorConfigArray = Array.isArray(fileConfig.fields);
2516
2713
  const configKeys = isGeneratorConfigArray ? [] : Object.keys(fileConfig.fields);
2517
2714
  const csvOptions = { separator: fileConfig.separator };
@@ -2523,10 +2720,10 @@ async function* rowGeneratorFromCsv(path2, fileConfig) {
2523
2720
  let finalInputStream;
2524
2721
  const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
2525
2722
  if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
2526
- initialReadStream = createReadStream(path2);
2723
+ initialReadStream = createReadStream(path7);
2527
2724
  finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
2528
2725
  } else {
2529
- initialReadStream = createReadStream(path2);
2726
+ initialReadStream = createReadStream(path7);
2530
2727
  finalInputStream = initialReadStream.pipe(createStripBomTransform());
2531
2728
  }
2532
2729
  finalInputStream.pipe(csvStream);
@@ -2568,7 +2765,7 @@ async function countCsvLines(filePath) {
2568
2765
  }).on("end", () => resolve(Math.max(0, newlines - 1))).on("error", reject);
2569
2766
  });
2570
2767
  }
2571
- var checkCsvHeaderRow = async (path2, fileConfig) => {
2768
+ var checkCsvHeaderRow = async (path7, fileConfig) => {
2572
2769
  return new Promise((resolve, reject) => {
2573
2770
  const colsWithParsingConfig = Object.keys(fileConfig.fields).map((key) => key.trim());
2574
2771
  console.debug(`${logPrefix} CSV Expected columns:`, colsWithParsingConfig.map((col) => {
@@ -2585,10 +2782,10 @@ var checkCsvHeaderRow = async (path2, fileConfig) => {
2585
2782
  let finalInputStream;
2586
2783
  const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
2587
2784
  if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
2588
- initialReadStream = createReadStream(path2);
2785
+ initialReadStream = createReadStream(path7);
2589
2786
  finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
2590
2787
  } else {
2591
- initialReadStream = createReadStream(path2);
2788
+ initialReadStream = createReadStream(path7);
2592
2789
  finalInputStream = initialReadStream.pipe(createStripBomTransform());
2593
2790
  }
2594
2791
  let isFirstLine = true;
@@ -2614,7 +2811,7 @@ var checkCsvHeaderRow = async (path2, fileConfig) => {
2614
2811
  i,
2615
2812
  col,
2616
2813
  headers[i] || "<undefined>",
2617
- path2
2814
+ path7
2618
2815
  ));
2619
2816
  }
2620
2817
  });
@@ -2631,7 +2828,7 @@ var checkCsvHeaderRow = async (path2, fileConfig) => {
2631
2828
  throw new Error(format4(
2632
2829
  `CSV missing expected column '%s' in file: %s`,
2633
2830
  col,
2634
- path2
2831
+ path7
2635
2832
  ));
2636
2833
  }
2637
2834
  });
@@ -2657,7 +2854,7 @@ var writeDataToCsv = async (fileName, data) => {
2657
2854
  try {
2658
2855
  const firstRow = data[0];
2659
2856
  if (!firstRow) {
2660
- console.log(`${logPrefix2} No data to write to CSV file=%s`, fileName);
2857
+ logger.info(`${logPrefix2} No data to write to CSV file=%s`, fileName);
2661
2858
  return;
2662
2859
  }
2663
2860
  const csvWriter = createObjectCsvWriter({
@@ -2667,7 +2864,7 @@ var writeDataToCsv = async (fileName, data) => {
2667
2864
  })
2668
2865
  });
2669
2866
  await csvWriter.writeRecords(data);
2670
- console.log(`${logPrefix2} CSV file=%s written successfully`, fileName);
2867
+ logger.info(`${logPrefix2} CSV file=%s written successfully`, fileName);
2671
2868
  } catch (err) {
2672
2869
  if (err instanceof Error) {
2673
2870
  throw new Error(format5(`error while writing csv file=%s, err: %s`, fileName, err.message));
@@ -2675,15 +2872,17 @@ var writeDataToCsv = async (fileName, data) => {
2675
2872
  throw err;
2676
2873
  }
2677
2874
  };
2678
- var writeGeneratorToCSV = async (generator, outputFileName) => {
2875
+ var writeGeneratorToCSV = async (generator, outputFileName, options) => {
2876
+ const batchSize = options?.batchSize ?? 1e4;
2877
+ const alwaysQuote = options?.alwaysQuote ?? true;
2679
2878
  try {
2680
2879
  let rowCount = 0;
2681
2880
  const firstDataRow = (await generator.next()).value;
2682
2881
  if (!firstDataRow) {
2683
- console.log(`${logPrefix2} [%s] first data row of csv empty, skipping`, outputFileName);
2882
+ logger.info(`${logPrefix2} [%s] first data row of csv empty, skipping`, outputFileName);
2684
2883
  return 0;
2685
2884
  }
2686
- console.log(`${logPrefix2} [%s] inferring headers from first data row`, outputFileName);
2885
+ logger.info(`${logPrefix2} [%s] inferring headers from first data row`, outputFileName);
2687
2886
  const headers = Object.keys(firstDataRow).map((col) => {
2688
2887
  return {
2689
2888
  id: col,
@@ -2693,14 +2892,14 @@ var writeGeneratorToCSV = async (generator, outputFileName) => {
2693
2892
  const csvWriter = createObjectCsvWriter({
2694
2893
  path: outputFileName,
2695
2894
  header: headers,
2696
- alwaysQuote: true
2895
+ alwaysQuote
2697
2896
  });
2698
- console.log(`${logPrefix2} [%s] writing csv`, outputFileName);
2897
+ logger.info(`${logPrefix2} [%s] writing csv`, outputFileName);
2699
2898
  let batch = [firstDataRow];
2700
2899
  for await (const data of generator) {
2701
2900
  batch.push(data);
2702
2901
  rowCount++;
2703
- if (batch.length > 1e4) {
2902
+ if (batch.length >= batchSize) {
2704
2903
  try {
2705
2904
  await csvWriter.writeRecords(batch);
2706
2905
  } catch (err) {
@@ -2718,8 +2917,10 @@ var writeGeneratorToCSV = async (generator, outputFileName) => {
2718
2917
  batch = [];
2719
2918
  }
2720
2919
  }
2721
- await csvWriter.writeRecords(batch);
2722
- console.log(`${logPrefix2} [%s] csv written successfully with %d rows`, outputFileName, rowCount);
2920
+ if (batch.length > 0) {
2921
+ await csvWriter.writeRecords(batch);
2922
+ }
2923
+ logger.info(`${logPrefix2} [%s] csv written successfully with %d rows`, outputFileName, rowCount);
2723
2924
  return rowCount;
2724
2925
  } catch (err) {
2725
2926
  if (err instanceof Error) {
@@ -3005,9 +3206,65 @@ var FilePatterns = class {
3005
3206
  };
3006
3207
 
3007
3208
  // src/services/sftp.ts
3209
+ import path4 from "path";
3210
+ import fs5 from "fs-extra";
3008
3211
  import Client from "ssh2-sftp-client";
3009
3212
  import { default as default2 } from "ssh2-sftp-client";
3010
3213
  var SftpService = new Client();
3214
+ var logPrefix3 = "[sftp]";
3215
+ async function listFilesRecursively(client, remotePath, options) {
3216
+ const recursive = options?.recursive ?? true;
3217
+ const extensions = options?.extensions?.map((e) => e.toLowerCase());
3218
+ const maxIterations = options?.maxIterations ?? 1e3;
3219
+ const results = [];
3220
+ const dirs = [remotePath];
3221
+ let iterations = 0;
3222
+ while (dirs.length > 0) {
3223
+ if (iterations++ >= maxIterations) {
3224
+ logger.warn(`${logPrefix3} listFilesRecursively reached maxIterations (${maxIterations}), stopping.`);
3225
+ break;
3226
+ }
3227
+ const currentDir = dirs.shift();
3228
+ let entries;
3229
+ try {
3230
+ entries = await client.list(currentDir);
3231
+ } catch (err) {
3232
+ const msg = err instanceof Error ? err.message : String(err);
3233
+ logger.warn(`${logPrefix3} Failed to list ${currentDir}: ${msg}`);
3234
+ continue;
3235
+ }
3236
+ for (const entry of entries) {
3237
+ if (entry.type === "d") {
3238
+ if (recursive && entry.name !== "." && entry.name !== "..") {
3239
+ dirs.push(path4.posix.join(currentDir, entry.name));
3240
+ }
3241
+ continue;
3242
+ }
3243
+ if (entry.type !== "-") continue;
3244
+ if (extensions) {
3245
+ const ext = path4.extname(entry.name).toLowerCase();
3246
+ if (!extensions.includes(ext)) continue;
3247
+ }
3248
+ const fullPath = path4.posix.join(currentDir, entry.name);
3249
+ results.push({ ...entry, name: fullPath });
3250
+ }
3251
+ }
3252
+ logger.info(`${logPrefix3} listFilesRecursively found ${results.length} files under ${remotePath}`);
3253
+ return results;
3254
+ }
3255
+ async function downloadFiles(client, files, localDir, options) {
3256
+ const concurrency = options?.concurrency ?? 5;
3257
+ await fs5.ensureDir(localDir);
3258
+ const tasks = files.map((file, index) => () => {
3259
+ const localName = `${index}_${path4.basename(file.name)}`;
3260
+ const localPath = path4.join(localDir, localName);
3261
+ logger.debug(`${logPrefix3} Downloading ${file.name} \u2192 ${localPath}`);
3262
+ return client.fastGet(file.name, localPath).then(() => localPath);
3263
+ });
3264
+ const results = await runWithConcurrency(tasks, concurrency);
3265
+ logger.info(`${logPrefix3} Downloaded ${results.length} files to ${localDir}`);
3266
+ return results;
3267
+ }
3011
3268
 
3012
3269
  // src/services/cloud-storage.ts
3013
3270
  import { Storage } from "@google-cloud/storage";
@@ -3015,7 +3272,7 @@ import { format as format6 } from "util";
3015
3272
  import * as pathModule from "path";
3016
3273
  import { existsSync, mkdirSync, unlinkSync as unlinkSync2 } from "fs";
3017
3274
  import { randomUUID } from "crypto";
3018
- var logPrefix3 = "[CloudStorageService]";
3275
+ var logPrefix4 = "[CloudStorageService]";
3019
3276
  var tmpDir = "tmp";
3020
3277
  var CloudStorageService = class {
3021
3278
  storage;
@@ -3023,10 +3280,10 @@ var CloudStorageService = class {
3023
3280
  processedSuffix;
3024
3281
  supportedExtensions;
3025
3282
  path;
3026
- constructor(bucketName, path2, opts) {
3283
+ constructor(bucketName, path7, opts) {
3027
3284
  this.storage = new Storage({ retryOptions: { autoRetry: true, maxRetries: 20 } });
3028
3285
  this.bucket = this.storage.bucket(bucketName);
3029
- this.path = path2;
3286
+ this.path = path7;
3030
3287
  this.processedSuffix = opts?.processedSuffix ?? ".processed";
3031
3288
  this.supportedExtensions = opts?.supportedExtensions ?? [];
3032
3289
  }
@@ -3067,7 +3324,7 @@ var CloudStorageService = class {
3067
3324
  try {
3068
3325
  const file = this.bucket.file(markerFileName);
3069
3326
  await file.save(format6("Marked file %s as processed", fileName));
3070
- logger.info(`${logPrefix3} Marker file ${markerFileName} created successfully.`);
3327
+ logger.info(`${logPrefix4} Marker file ${markerFileName} created successfully.`);
3071
3328
  } catch (err) {
3072
3329
  if (err instanceof Error) {
3073
3330
  throw new Error(format6(`error while writing marker file=%s, err:%s`, markerFileName, err.message));
@@ -3091,7 +3348,7 @@ var CloudStorageService = class {
3091
3348
  unlinkSync2(destFilename);
3092
3349
  }
3093
3350
  await file.download({ destination: destFilename });
3094
- logger.info(`${logPrefix3} Downloaded ${fileName} to ${destFilename}`);
3351
+ logger.info(`${logPrefix4} Downloaded ${fileName} to ${destFilename}`);
3095
3352
  return destFilename;
3096
3353
  } catch (err) {
3097
3354
  if (err instanceof Error) {
@@ -3109,9 +3366,9 @@ var CloudStorageService = class {
3109
3366
  */
3110
3367
  async uploadFile(localPath, destPath) {
3111
3368
  try {
3112
- logger.info(`${logPrefix3} preparing to upload '%s' to '%s'`, localPath, destPath);
3369
+ logger.info(`${logPrefix4} preparing to upload '%s' to '%s'`, localPath, destPath);
3113
3370
  const [file] = await this.bucket.upload(localPath, { destination: destPath });
3114
- logger.info(`${logPrefix3} file %s has been successfully uploaded into %s`, localPath, destPath);
3371
+ logger.info(`${logPrefix4} file %s has been successfully uploaded into %s`, localPath, destPath);
3115
3372
  return file;
3116
3373
  } catch (err) {
3117
3374
  if (err instanceof Error) {
@@ -3187,6 +3444,376 @@ var unzip = async (zipFilePath, extractedPath) => {
3187
3444
  return extractedPath;
3188
3445
  };
3189
3446
 
3447
+ // src/services/cdn.ts
3448
+ import axios3 from "axios";
3449
+ import axiosRetry2 from "axios-retry";
3450
+ var logPrefix5 = "[CdnService]";
3451
+ var MAX_RETRIES = 10;
3452
+ var CdnService = class {
3453
+ axiosClient;
3454
+ constructor(config) {
3455
+ const resolvedKey = getServiceAccountKey(config.serviceAccountKey);
3456
+ const resolvedEndpoint = getApiEndpoint(config.apiEndpoint);
3457
+ this.axiosClient = axios3.create({
3458
+ baseURL: resolvedEndpoint,
3459
+ timeout: 12e4,
3460
+ headers: {
3461
+ Authorization: `Bearer ${resolvedKey}`,
3462
+ Accept: "application/json"
3463
+ }
3464
+ });
3465
+ axiosRetry2(this.axiosClient, {
3466
+ retries: MAX_RETRIES,
3467
+ retryCondition: (error) => {
3468
+ const status = error.response?.status ?? 0;
3469
+ return !error.response || status === 429 || status >= 500 && status < 600;
3470
+ },
3471
+ retryDelay: (retryCount, error) => {
3472
+ const retryAfter = error.response?.headers?.["retry-after"];
3473
+ if (retryAfter) {
3474
+ const seconds = Number(retryAfter);
3475
+ if (!isNaN(seconds) && seconds > 0) {
3476
+ logger.info(`${logPrefix5} Rate limited \u2014 waiting ${seconds}s (Retry-After header), attempt ${retryCount}/${MAX_RETRIES}`);
3477
+ return seconds * 1e3;
3478
+ }
3479
+ }
3480
+ const delay = axiosRetry2.exponentialDelay(retryCount);
3481
+ logger.info(`${logPrefix5} Retrying in ${delay}ms (attempt ${retryCount}/${MAX_RETRIES}) for ${error.config?.method?.toUpperCase()} ${error.config?.url}`);
3482
+ return delay;
3483
+ }
3484
+ });
3485
+ }
3486
+ /**
3487
+ * Returns metadata for a CDN file via a HEAD request.
3488
+ * If the file does not exist, returns `{ exists: false, lastModified: null, contentType: null }`.
3489
+ * Falls back to not-existing if HEAD is not supported (405).
3490
+ */
3491
+ async getFileMetadata(cdnId, fileName) {
3492
+ const url = `/v1/cdn/${cdnId}/files/${encodeURIComponent(fileName)}`;
3493
+ try {
3494
+ const response = await this.axiosClient.head(url);
3495
+ const lastModifiedHeader = response.headers["last-modified"];
3496
+ const contentTypeHeader = response.headers["content-type"];
3497
+ return {
3498
+ exists: true,
3499
+ lastModified: lastModifiedHeader ? new Date(lastModifiedHeader) : null,
3500
+ contentType: contentTypeHeader ?? null
3501
+ };
3502
+ } catch (err) {
3503
+ if (axios3.isAxiosError(err)) {
3504
+ const status = err.response?.status;
3505
+ if (status === 404) return { exists: false, lastModified: null, contentType: null };
3506
+ if (status === 405) {
3507
+ logger.warn(`${logPrefix5} HEAD not supported for ${url}, treating file as absent`);
3508
+ return { exists: false, lastModified: null, contentType: null };
3509
+ }
3510
+ this.throwMeaningfulError(err);
3511
+ }
3512
+ throw err;
3513
+ }
3514
+ }
3515
+ /**
3516
+ * Checks whether a file already exists in the CDN via a HEAD request.
3517
+ * Returns true if the server responds 2xx, false on 404.
3518
+ * Falls back to false (treat as not existing) if HEAD is not supported (405).
3519
+ * No body is transferred.
3520
+ */
3521
+ async fileExists(cdnId, fileName) {
3522
+ return (await this.getFileMetadata(cdnId, fileName)).exists;
3523
+ }
3524
+ /**
3525
+ * Uploads a file buffer to the Whaly CDN.
3526
+ * The PUT is idempotent — uploading the same fileName overwrites the existing file.
3527
+ */
3528
+ async uploadFile(cdnId, fileName, fileBuffer) {
3529
+ const url = `/v1/cdn/${cdnId}/files/${encodeURIComponent(fileName)}`;
3530
+ try {
3531
+ logger.info(`${logPrefix5} Uploading ${fileName} (${fileBuffer.length} bytes) to CDN ${cdnId}`);
3532
+ const form = new FormData();
3533
+ form.append("file", new Blob([fileBuffer]), fileName);
3534
+ const response = await this.axiosClient.put(url, form);
3535
+ logger.info(`${logPrefix5} Successfully uploaded ${fileName} (status ${response.status})`);
3536
+ return {
3537
+ filePath: response.data?.filePath ?? `/org/${cdnId}/file/${fileName}`
3538
+ };
3539
+ } catch (err) {
3540
+ if (axios3.isAxiosError(err)) {
3541
+ this.throwMeaningfulError(err);
3542
+ }
3543
+ throw err;
3544
+ }
3545
+ }
3546
+ throwMeaningfulError(err) {
3547
+ const status = err.response?.status;
3548
+ if (status === 401 || status === 403) {
3549
+ throw new Error("Unauthorized - invalid or expired service account key");
3550
+ } else if (status === 404) {
3551
+ throw new Error("Organization or CDN not found");
3552
+ } else if (status === 500) {
3553
+ throw new Error("API server error - check CDN ID and service account permissions");
3554
+ } else {
3555
+ throw new Error(`API error: ${err.response?.statusText ?? err.message}`);
3556
+ }
3557
+ }
3558
+ };
3559
+
3560
+ // src/sdk/models/asset-tap/asset-stream.ts
3561
+ var AssetStream = class {
3562
+ streamId;
3563
+ config;
3564
+ replicationMode;
3565
+ constructor(streamId, config, replicationMode = "INCREMENTAL") {
3566
+ this.streamId = streamId;
3567
+ this.config = config;
3568
+ this.replicationMode = replicationMode;
3569
+ }
3570
+ async transformFile(downloadedPath, _entry) {
3571
+ return downloadedPath;
3572
+ }
3573
+ };
3574
+
3575
+ // src/sdk/models/asset-tap/asset-tap.ts
3576
+ import fs6 from "fs-extra";
3577
+ import path5 from "path";
3578
+ var logPrefix6 = "[AssetTap]";
3579
+ function inferContentType(filePath, fallback) {
3580
+ const mime = getMimeType(filePath);
3581
+ return mime !== "application/octet-stream" ? mime : fallback;
3582
+ }
3583
+ function deriveManifestMode(streams) {
3584
+ const modes = new Set(streams.map((s) => s.replicationMode));
3585
+ if (modes.size > 1) {
3586
+ logger.warn(`${logPrefix6} Streams have mixed replication modes (${[...modes].join(", ")}). Manifest will report "FULL".`);
3587
+ return "FULL";
3588
+ }
3589
+ return streams[0]?.replicationMode ?? "INCREMENTAL";
3590
+ }
3591
+ var AssetTap = class {
3592
+ config;
3593
+ outputDir;
3594
+ streams = [];
3595
+ concurrency;
3596
+ target;
3597
+ constructor(target, config, outputDir = "out", concurrency = 5) {
3598
+ this.target = target;
3599
+ this.config = config;
3600
+ this.outputDir = outputDir;
3601
+ this.concurrency = concurrency;
3602
+ }
3603
+ async sync() {
3604
+ await this.init();
3605
+ const dryRun = isDryRun();
3606
+ const dryRunLimit = dryRun ? getDryRunLimit() : void 0;
3607
+ if (dryRun) {
3608
+ logger.info(`${logPrefix6} [DRY_RUN] mode active \u2014 skipping CDN checks and uploads`);
3609
+ await fs6.emptyDir(this.outputDir);
3610
+ if (dryRunLimit !== void 0) {
3611
+ logger.info(`${logPrefix6} [DRY_RUN] Limit: ${dryRunLimit} assets per stream`);
3612
+ }
3613
+ } else if (process.env["DRY_RUN_LIMIT"] !== void 0) {
3614
+ logger.warn(`${logPrefix6} DRY_RUN_LIMIT is set but DRY_RUN is not active \u2014 limit will be ignored`);
3615
+ }
3616
+ const tmpDir2 = path5.join(this.outputDir, "tmp");
3617
+ await fs6.ensureDir(tmpDir2);
3618
+ const streamManifests = [];
3619
+ let entryIndex = 0;
3620
+ let totalSummary = { total: 0, uploaded: 0, skipped: 0, errors: 0 };
3621
+ for (const stream of this.streams) {
3622
+ logger.info(`${logPrefix6} Processing stream: ${stream.streamId} (mode=${stream.replicationMode}, concurrency=${this.concurrency})`);
3623
+ const assetEntries = [];
3624
+ let streamAssetCount = 0;
3625
+ await processFromAsyncIterable(
3626
+ stream.listAssets(),
3627
+ async (entry) => {
3628
+ if (dryRunLimit !== void 0 && streamAssetCount >= dryRunLimit) {
3629
+ return "stop";
3630
+ }
3631
+ logger.debug(`${logPrefix6} Processing entry: ${entry.sourcePath}`);
3632
+ if (stream.replicationMode === "INCREMENTAL" && !dryRun) {
3633
+ const shouldSync = await this.target.shouldSync(entry);
3634
+ if (!shouldSync) {
3635
+ logger.debug(`${logPrefix6} Skipping ${entry.sourcePath} (up-to-date)`);
3636
+ assetEntries.push({
3637
+ sourcePath: entry.sourcePath,
3638
+ destinationPath: entry.destinationPath,
3639
+ downloadedPath: "",
3640
+ transformedPath: "",
3641
+ size: 0,
3642
+ contentType: entry.contentType,
3643
+ status: "skipped",
3644
+ transformed: false
3645
+ });
3646
+ return "continue";
3647
+ }
3648
+ }
3649
+ const fileName = `${entryIndex}_${path5.basename(entry.sourcePath)}`;
3650
+ const downloadedPath = path5.join(tmpDir2, fileName);
3651
+ let uploadPath = downloadedPath;
3652
+ entryIndex++;
3653
+ streamAssetCount++;
3654
+ try {
3655
+ await stream.downloadEntry(entry, downloadedPath);
3656
+ uploadPath = await stream.transformFile(downloadedPath, entry);
3657
+ const wasTransformed = uploadPath !== downloadedPath;
3658
+ const stat = await fs6.stat(uploadPath);
3659
+ const processed = {
3660
+ entry,
3661
+ downloadedPath,
3662
+ uploadPath,
3663
+ wasTransformed,
3664
+ size: stat.size,
3665
+ contentType: wasTransformed ? inferContentType(uploadPath, entry.contentType) : entry.contentType
3666
+ };
3667
+ if (!dryRun) {
3668
+ await this.target.uploadAsset(processed);
3669
+ } else {
3670
+ const inspectPath = path5.join(this.outputDir, stream.streamId, entry.destinationPath);
3671
+ await fs6.ensureDir(path5.dirname(inspectPath));
3672
+ await fs6.copy(uploadPath, inspectPath);
3673
+ }
3674
+ assetEntries.push({
3675
+ sourcePath: entry.sourcePath,
3676
+ destinationPath: entry.destinationPath,
3677
+ downloadedPath,
3678
+ transformedPath: wasTransformed ? uploadPath : "",
3679
+ size: processed.size,
3680
+ contentType: processed.contentType,
3681
+ status: "uploaded",
3682
+ transformed: wasTransformed
3683
+ });
3684
+ logger.info(`${logPrefix6} ${dryRun ? "[DRY_RUN] Processed" : "Uploaded"} ${entry.sourcePath} \u2192 ${entry.destinationPath}`);
3685
+ } catch (err) {
3686
+ const message = err instanceof Error ? err.message : String(err);
3687
+ logger.error(`${logPrefix6} Failed to process ${entry.sourcePath}: ${message}`);
3688
+ assetEntries.push({
3689
+ sourcePath: entry.sourcePath,
3690
+ destinationPath: entry.destinationPath,
3691
+ downloadedPath: "",
3692
+ transformedPath: "",
3693
+ size: 0,
3694
+ contentType: entry.contentType,
3695
+ status: "error",
3696
+ transformed: false,
3697
+ error: message
3698
+ });
3699
+ } finally {
3700
+ await fs6.remove(downloadedPath).catch(() => void 0);
3701
+ if (uploadPath !== downloadedPath) {
3702
+ await fs6.remove(uploadPath).catch(() => void 0);
3703
+ }
3704
+ }
3705
+ if (dryRunLimit !== void 0 && streamAssetCount >= dryRunLimit) {
3706
+ logger.info(`${logPrefix6} [DRY_RUN] Reached limit of ${dryRunLimit} for stream "${stream.streamId}", stopping.`);
3707
+ return "stop";
3708
+ }
3709
+ return "continue";
3710
+ },
3711
+ this.concurrency
3712
+ );
3713
+ const streamSummary = {
3714
+ total: assetEntries.length,
3715
+ uploaded: assetEntries.filter((a) => a.status === "uploaded").length,
3716
+ skipped: assetEntries.filter((a) => a.status === "skipped").length,
3717
+ errors: assetEntries.filter((a) => a.status === "error").length
3718
+ };
3719
+ totalSummary.total += streamSummary.total;
3720
+ totalSummary.uploaded += streamSummary.uploaded;
3721
+ totalSummary.skipped += streamSummary.skipped;
3722
+ totalSummary.errors += streamSummary.errors;
3723
+ streamManifests.push({
3724
+ streamId: stream.streamId,
3725
+ mode: stream.replicationMode,
3726
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
3727
+ assets: assetEntries,
3728
+ summary: streamSummary
3729
+ });
3730
+ }
3731
+ const manifest = {
3732
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
3733
+ mode: deriveManifestMode(this.streams),
3734
+ streams: streamManifests,
3735
+ summary: totalSummary
3736
+ };
3737
+ await fs6.ensureDir(this.outputDir);
3738
+ await fs6.writeJson(path5.join(this.outputDir, "manifest.json"), manifest, { spaces: 2 });
3739
+ if (!dryRun) {
3740
+ await this.target.complete();
3741
+ }
3742
+ logger.info(`${logPrefix6} Sync complete. Uploaded=${totalSummary.uploaded} Skipped=${totalSummary.skipped} Errors=${totalSummary.errors}`);
3743
+ return manifest;
3744
+ }
3745
+ };
3746
+
3747
+ // src/sdk/models/asset-tap/image-transform.ts
3748
+ import sharp from "sharp";
3749
+ import path6 from "path";
3750
+ var SHARP_GRAVITY = {
3751
+ center: "centre",
3752
+ top: "north",
3753
+ bottom: "south",
3754
+ left: "west",
3755
+ right: "east",
3756
+ "top left": "northwest",
3757
+ "top right": "northeast",
3758
+ "bottom left": "southwest",
3759
+ "bottom right": "southeast"
3760
+ };
3761
+ function parseBackground(bg) {
3762
+ if (bg === void 0 || bg === "none" || bg === "transparent") {
3763
+ return { r: 0, g: 0, b: 0, alpha: 0 };
3764
+ }
3765
+ return bg;
3766
+ }
3767
+ var ImageTransform = class {
3768
+ /**
3769
+ * Converts an image to WebP format using sharp (libvips).
3770
+ *
3771
+ * The output file is written to the same directory as the input,
3772
+ * with the extension replaced by `.webp`.
3773
+ *
3774
+ * @param inputPath Path to the source image file.
3775
+ * @param options Optional resize / padding parameters.
3776
+ * @returns Path to the produced `.webp` file.
3777
+ */
3778
+ static async toWebp(inputPath, options) {
3779
+ const dir = path6.dirname(inputPath);
3780
+ const basename = path6.basename(inputPath, path6.extname(inputPath));
3781
+ const outputPath = path6.join(dir, `${basename}.webp`);
3782
+ let pipeline = sharp(inputPath);
3783
+ const hasSize = options.width !== void 0 && options.height !== void 0;
3784
+ if (hasSize) {
3785
+ const position = options.gravity !== void 0 ? SHARP_GRAVITY[options.gravity] ?? options.gravity : void 0;
3786
+ if (options.extent === true) {
3787
+ pipeline = pipeline.resize(options.width, options.height, {
3788
+ fit: "contain",
3789
+ position,
3790
+ background: parseBackground(options.background)
3791
+ });
3792
+ } else {
3793
+ pipeline = pipeline.resize(options.width, options.height, {
3794
+ fit: "inside",
3795
+ position
3796
+ });
3797
+ }
3798
+ }
3799
+ pipeline = pipeline.webp(
3800
+ options.quality !== void 0 ? { quality: options.quality } : {}
3801
+ );
3802
+ await pipeline.toFile(outputPath);
3803
+ return outputPath;
3804
+ }
3805
+ };
3806
+
3807
+ // src/sdk/models/asset-target/asset-target.ts
3808
+ var AssetTarget = class {
3809
+ config;
3810
+ constructor(config) {
3811
+ this.config = config;
3812
+ }
3813
+ async complete() {
3814
+ }
3815
+ };
3816
+
3190
3817
  // src/targets/bigquery/helpers.ts
3191
3818
  var safeColumnName = (key) => {
3192
3819
  let returnString = key;
@@ -3223,7 +3850,9 @@ import Decimal from "decimal.js";
3223
3850
  import dayjs5 from "dayjs";
3224
3851
  var validateDateRange = (record, schema) => {
3225
3852
  Object.keys(schema).forEach((key) => {
3226
- const { format: format8 } = schema[key];
3853
+ const field = schema[key];
3854
+ if (!field) return;
3855
+ const { format: format8 } = field;
3227
3856
  if (format8 === "date-time") {
3228
3857
  if (record[key]) {
3229
3858
  const formattedDate = dayjs5(record[key]).format(defaultDateTimeFormat);
@@ -3740,6 +4369,46 @@ var BigQueryTarget = class extends ITarget {
3740
4369
  convertNumberIntoDecimal = convertNumberIntoDecimal;
3741
4370
  };
3742
4371
 
4372
+ // src/targets/cdn/main.ts
4373
+ import fs7 from "fs-extra";
4374
+ var logPrefix7 = "[CdnAssetTarget]";
4375
+ var CdnAssetTarget = class extends AssetTarget {
4376
+ cdnService;
4377
+ constructor(config) {
4378
+ super(config);
4379
+ this.cdnService = new CdnService(config);
4380
+ }
4381
+ async shouldSync(entry) {
4382
+ const metadata = await this.cdnService.getFileMetadata(getCdnId(this.config.cdnId), entry.destinationPath);
4383
+ if (!metadata.exists) {
4384
+ logger.debug(`${logPrefix7} ${entry.destinationPath} not in CDN \u2192 will sync`);
4385
+ return true;
4386
+ }
4387
+ if (entry.lastModified === void 0) {
4388
+ logger.debug(`${logPrefix7} ${entry.destinationPath} source has no lastModified \u2192 will sync`);
4389
+ return true;
4390
+ }
4391
+ if (metadata.lastModified === null) {
4392
+ logger.debug(`${logPrefix7} ${entry.destinationPath} CDN has no lastModified \u2192 will sync`);
4393
+ return true;
4394
+ }
4395
+ const sourceIsNewer = entry.lastModified > metadata.lastModified;
4396
+ if (!sourceIsNewer) {
4397
+ logger.debug(`${logPrefix7} Skipping ${entry.destinationPath} (CDN is up-to-date)`);
4398
+ }
4399
+ return sourceIsNewer;
4400
+ }
4401
+ async uploadAsset(asset) {
4402
+ const fileBuffer = await fs7.readFile(asset.uploadPath);
4403
+ await this.cdnService.uploadFile(
4404
+ getCdnId(this.config.cdnId),
4405
+ asset.entry.destinationPath,
4406
+ fileBuffer
4407
+ );
4408
+ logger.info(`${logPrefix7} Uploaded ${asset.entry.destinationPath} (${fileBuffer.length} bytes)`);
4409
+ }
4410
+ };
4411
+
3743
4412
  // src/state-providers/gcs/main.ts
3744
4413
  import { format as format7 } from "util";
3745
4414
  var GCSStateProvider = class {
@@ -3780,9 +4449,14 @@ var GCSStateProvider = class {
3780
4449
  export {
3781
4450
  ALL_STREAM_ID_KEYWORD,
3782
4451
  API_CALLS_METRIC_NAME,
4452
+ AssetStream,
4453
+ AssetTap,
4454
+ AssetTarget,
3783
4455
  Authenticator,
3784
4456
  BATCH_INTERVAL_MS,
3785
4457
  BigQueryTarget,
4458
+ CdnAssetTarget,
4459
+ CdnService,
3786
4460
  CloudStorageService,
3787
4461
  CounterMetric,
3788
4462
  CsvExtractionConfigBuilder,
@@ -3796,6 +4470,8 @@ export {
3796
4470
  FileTap,
3797
4471
  GCSStateProvider,
3798
4472
  ITarget,
4473
+ ImageTransform,
4474
+ MIME_TYPES,
3799
4475
  MissingFieldInSchemaError,
3800
4476
  MissingSchemaError,
3801
4477
  RESTStream,
@@ -3817,6 +4493,7 @@ export {
3817
4493
  _greaterThan,
3818
4494
  addWhalyFields,
3819
4495
  checkCsvHeaderRow,
4496
+ collectPaginated,
3820
4497
  countCsvLines,
3821
4498
  createCellReference,
3822
4499
  createCsvStreamConfig,
@@ -3824,6 +4501,7 @@ export {
3824
4501
  createExcelStreamConfig,
3825
4502
  createTemporaryFileStream,
3826
4503
  csvFieldsToJsonSchema,
4504
+ downloadFiles,
3827
4505
  excelColumnToIndex,
3828
4506
  excelFieldsToJsonSchema,
3829
4507
  excelRowToIndex,
@@ -3836,29 +4514,40 @@ export {
3836
4514
  findAllMatchingConfigs,
3837
4515
  flattenSchema,
3838
4516
  getAllMetrics,
4517
+ getApiEndpoint,
3839
4518
  getAxiosInstance,
4519
+ getCdnId,
3840
4520
  getCounterMetric,
3841
4521
  getCounterMetrics,
3842
4522
  getDownloadFileApiCall,
4523
+ getDryRunLimit,
3843
4524
  getJSONApiCall,
3844
4525
  getJSONApiCallWithFullResponse,
4526
+ getMimeType,
3845
4527
  getRawJSONApiCall,
4528
+ getServiceAccountKey,
3846
4529
  gracefulExit,
3847
4530
  haltAndCatchFire,
3848
4531
  incrementStreamState,
3849
4532
  indexToExcelColumn,
3850
4533
  indexToExcelRow,
4534
+ isDryRun,
4535
+ listFilesRecursively,
3851
4536
  loadJson,
3852
4537
  loadSchema,
3853
4538
  logger,
4539
+ optionalEnv,
3854
4540
  parseCellReference,
3855
4541
  postFormDataApiCall,
3856
4542
  postJSONApiCall,
3857
4543
  postUrlEncodedApiCall,
3858
4544
  printMemoryFootprint,
3859
4545
  processFileStreams,
4546
+ processFromAsyncIterable,
3860
4547
  removeParasiteProperties,
4548
+ requireEnv,
3861
4549
  rowGeneratorFromCsv,
4550
+ runWithConcurrency,
3862
4551
  safePath,
3863
4552
  unzip,
3864
4553
  validateExcelConfig,