@whaly/connector-sdk 0.3.4 → 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.js CHANGED
@@ -32,9 +32,14 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  ALL_STREAM_ID_KEYWORD: () => ALL_STREAM_ID_KEYWORD,
34
34
  API_CALLS_METRIC_NAME: () => API_CALLS_METRIC_NAME,
35
+ AssetStream: () => AssetStream,
36
+ AssetTap: () => AssetTap,
37
+ AssetTarget: () => AssetTarget,
35
38
  Authenticator: () => Authenticator,
36
39
  BATCH_INTERVAL_MS: () => BATCH_INTERVAL_MS,
37
40
  BigQueryTarget: () => BigQueryTarget,
41
+ CdnAssetTarget: () => CdnAssetTarget,
42
+ CdnService: () => CdnService,
38
43
  CloudStorageService: () => CloudStorageService,
39
44
  CounterMetric: () => CounterMetric,
40
45
  CsvExtractionConfigBuilder: () => CsvExtractionConfigBuilder,
@@ -48,6 +53,8 @@ __export(index_exports, {
48
53
  FileTap: () => FileTap,
49
54
  GCSStateProvider: () => GCSStateProvider,
50
55
  ITarget: () => ITarget,
56
+ ImageTransform: () => ImageTransform,
57
+ MIME_TYPES: () => MIME_TYPES,
51
58
  MissingFieldInSchemaError: () => MissingFieldInSchemaError,
52
59
  MissingSchemaError: () => MissingSchemaError,
53
60
  RESTStream: () => RESTStream,
@@ -69,6 +76,7 @@ __export(index_exports, {
69
76
  _greaterThan: () => _greaterThan,
70
77
  addWhalyFields: () => addWhalyFields,
71
78
  checkCsvHeaderRow: () => checkCsvHeaderRow,
79
+ collectPaginated: () => collectPaginated,
72
80
  countCsvLines: () => countCsvLines,
73
81
  createCellReference: () => createCellReference,
74
82
  createCsvStreamConfig: () => createCsvStreamConfig,
@@ -76,6 +84,7 @@ __export(index_exports, {
76
84
  createExcelStreamConfig: () => createExcelStreamConfig,
77
85
  createTemporaryFileStream: () => createTemporaryFileStream,
78
86
  csvFieldsToJsonSchema: () => csvFieldsToJsonSchema,
87
+ downloadFiles: () => downloadFiles,
79
88
  excelColumnToIndex: () => excelColumnToIndex,
80
89
  excelFieldsToJsonSchema: () => excelFieldsToJsonSchema,
81
90
  excelRowToIndex: () => excelRowToIndex,
@@ -88,29 +97,40 @@ __export(index_exports, {
88
97
  findAllMatchingConfigs: () => findAllMatchingConfigs,
89
98
  flattenSchema: () => flattenSchema,
90
99
  getAllMetrics: () => getAllMetrics,
100
+ getApiEndpoint: () => getApiEndpoint,
91
101
  getAxiosInstance: () => getAxiosInstance,
102
+ getCdnId: () => getCdnId,
92
103
  getCounterMetric: () => getCounterMetric,
93
104
  getCounterMetrics: () => getCounterMetrics,
94
105
  getDownloadFileApiCall: () => getDownloadFileApiCall,
106
+ getDryRunLimit: () => getDryRunLimit,
95
107
  getJSONApiCall: () => getJSONApiCall,
96
108
  getJSONApiCallWithFullResponse: () => getJSONApiCallWithFullResponse,
109
+ getMimeType: () => getMimeType,
97
110
  getRawJSONApiCall: () => getRawJSONApiCall,
111
+ getServiceAccountKey: () => getServiceAccountKey,
98
112
  gracefulExit: () => gracefulExit,
99
113
  haltAndCatchFire: () => haltAndCatchFire,
100
114
  incrementStreamState: () => incrementStreamState,
101
115
  indexToExcelColumn: () => indexToExcelColumn,
102
116
  indexToExcelRow: () => indexToExcelRow,
117
+ isDryRun: () => isDryRun,
118
+ listFilesRecursively: () => listFilesRecursively,
103
119
  loadJson: () => loadJson,
104
120
  loadSchema: () => loadSchema,
105
121
  logger: () => logger,
122
+ optionalEnv: () => optionalEnv,
106
123
  parseCellReference: () => parseCellReference,
107
124
  postFormDataApiCall: () => postFormDataApiCall,
108
125
  postJSONApiCall: () => postJSONApiCall,
109
126
  postUrlEncodedApiCall: () => postUrlEncodedApiCall,
110
127
  printMemoryFootprint: () => printMemoryFootprint,
111
128
  processFileStreams: () => processFileStreams,
129
+ processFromAsyncIterable: () => processFromAsyncIterable,
112
130
  removeParasiteProperties: () => removeParasiteProperties,
131
+ requireEnv: () => requireEnv,
113
132
  rowGeneratorFromCsv: () => rowGeneratorFromCsv,
133
+ runWithConcurrency: () => runWithConcurrency,
114
134
  safePath: () => safePath,
115
135
  unzip: () => unzip,
116
136
  validateExcelConfig: () => validateExcelConfig,
@@ -400,13 +420,26 @@ var postUrlEncodedApiCall = async (endpoint, config, payload) => {
400
420
  return response.data;
401
421
  } catch (err) {
402
422
  if (err.response.status > 400) {
403
- throw new Error(`We got a \`${err.response.status}\` status code from endpoint: \`${endpoint}\`.
404
-
423
+ throw new Error(`We got a \`${err.response.status}\` status code from endpoint: \`${endpoint}\`.
424
+
405
425
  Response body from the server: ${JSON.stringify(err.response.data)}`);
406
426
  }
407
427
  throw err;
408
428
  }
409
429
  };
430
+ async function collectPaginated(options) {
431
+ const { initialUrl, fetchPage, getRecords, getNextUrl } = options;
432
+ const results = [];
433
+ let currentUrl = initialUrl;
434
+ while (true) {
435
+ const response = await fetchPage(currentUrl);
436
+ results.push(...getRecords(response));
437
+ const nextUrl = getNextUrl(response);
438
+ if (nextUrl === void 0) break;
439
+ currentUrl = nextUrl;
440
+ }
441
+ return results;
442
+ }
410
443
 
411
444
  // src/sdk/service/metric.ts
412
445
  var metricStore = {};
@@ -497,6 +530,157 @@ var haltAndCatchFire = async (errorType, errorText, errorDebugText) => {
497
530
  await gracefulExit(0);
498
531
  };
499
532
 
533
+ // src/sdk/service/dryRun.ts
534
+ function isDryRun() {
535
+ const val = process.env["DRY_RUN"];
536
+ return val !== void 0 && val !== "" && val !== "0" && val !== "false";
537
+ }
538
+ function getDryRunLimit() {
539
+ const val = process.env["DRY_RUN_LIMIT"];
540
+ if (!val) return void 0;
541
+ const n = parseInt(val, 10);
542
+ return isNaN(n) || n <= 0 ? void 0 : n;
543
+ }
544
+
545
+ // src/sdk/service/apiEndpoint.ts
546
+ var ENV_VAR = "WLY_API_ENDPOINT";
547
+ function getApiEndpoint(explicit) {
548
+ const endpoint = explicit ?? process.env[ENV_VAR];
549
+ if (!endpoint) {
550
+ throw new Error(
551
+ `No API endpoint provided. Either pass "apiEndpoint" in config or set the ${ENV_VAR} environment variable.`
552
+ );
553
+ }
554
+ return endpoint;
555
+ }
556
+
557
+ // src/sdk/service/serviceAccountKey.ts
558
+ var ENV_VAR2 = "WLY_SERVICE_ACCOUNT_KEY";
559
+ function getServiceAccountKey(explicit) {
560
+ const key = explicit ?? process.env[ENV_VAR2];
561
+ if (!key) {
562
+ throw new Error(
563
+ `No service account key provided. Either pass "serviceAccountKey" in config or set the ${ENV_VAR2} environment variable.`
564
+ );
565
+ }
566
+ return key;
567
+ }
568
+
569
+ // src/sdk/service/cdnId.ts
570
+ var ENV_VAR3 = "WLY_CDN_ID";
571
+ function getCdnId(explicit) {
572
+ const cdnId = explicit ?? process.env[ENV_VAR3];
573
+ if (!cdnId) {
574
+ throw new Error(
575
+ `No CDN ID provided. Either pass "cdnId" in config or set the ${ENV_VAR3} environment variable.`
576
+ );
577
+ }
578
+ return cdnId;
579
+ }
580
+
581
+ // src/sdk/service/env.ts
582
+ var import_dotenv = __toESM(require("dotenv"));
583
+ var dotenvLoaded = false;
584
+ function ensureDotenv() {
585
+ if (!dotenvLoaded) {
586
+ import_dotenv.default.config();
587
+ dotenvLoaded = true;
588
+ }
589
+ }
590
+ function requireEnv(name) {
591
+ ensureDotenv();
592
+ const value = process.env[name];
593
+ if (!value) {
594
+ throw new Error(`Missing required environment variable: ${name}`);
595
+ }
596
+ return value;
597
+ }
598
+ function optionalEnv(name, defaultValue) {
599
+ ensureDotenv();
600
+ return process.env[name] ?? defaultValue;
601
+ }
602
+
603
+ // src/sdk/service/concurrency.ts
604
+ async function runWithConcurrency(tasks, concurrency) {
605
+ if (concurrency < 1) {
606
+ throw new Error(`concurrency must be >= 1, got ${concurrency}`);
607
+ }
608
+ const results = new Array(tasks.length);
609
+ let nextIndex = 0;
610
+ async function worker() {
611
+ while (nextIndex < tasks.length) {
612
+ const index = nextIndex++;
613
+ const task = tasks[index];
614
+ if (task) {
615
+ results[index] = await task();
616
+ }
617
+ }
618
+ }
619
+ const workers = Array.from(
620
+ { length: Math.min(concurrency, tasks.length) },
621
+ () => worker()
622
+ );
623
+ await Promise.all(workers);
624
+ return results;
625
+ }
626
+ async function processFromAsyncIterable(iterable, processor, concurrency, onError) {
627
+ if (concurrency < 1) {
628
+ throw new Error(`concurrency must be >= 1, got ${concurrency}`);
629
+ }
630
+ const iterator = iterable[Symbol.asyncIterator]();
631
+ let stopped = false;
632
+ async function worker() {
633
+ while (!stopped) {
634
+ const { value, done } = await iterator.next();
635
+ if (done) break;
636
+ try {
637
+ const result = await processor(value);
638
+ if (result === "stop") {
639
+ stopped = true;
640
+ break;
641
+ }
642
+ } catch (err) {
643
+ if (onError) {
644
+ onError(value, err);
645
+ } else {
646
+ stopped = true;
647
+ throw err;
648
+ }
649
+ }
650
+ }
651
+ }
652
+ const workers = Array.from({ length: concurrency }, () => worker());
653
+ await Promise.all(workers);
654
+ }
655
+
656
+ // src/sdk/service/mime.ts
657
+ var import_node_path = __toESM(require("path"));
658
+ var MIME_TYPES = {
659
+ ".webp": "image/webp",
660
+ ".png": "image/png",
661
+ ".jpg": "image/jpeg",
662
+ ".jpeg": "image/jpeg",
663
+ ".gif": "image/gif",
664
+ ".svg": "image/svg+xml",
665
+ ".avif": "image/avif",
666
+ ".bmp": "image/bmp",
667
+ ".tiff": "image/tiff",
668
+ ".tif": "image/tiff",
669
+ ".ico": "image/x-icon",
670
+ ".pdf": "application/pdf",
671
+ ".json": "application/json",
672
+ ".csv": "text/csv",
673
+ ".xml": "application/xml",
674
+ ".zip": "application/zip",
675
+ ".gz": "application/gzip",
676
+ ".xls": "application/vnd.ms-excel",
677
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
678
+ };
679
+ function getMimeType(filePathOrExt) {
680
+ const ext = filePathOrExt.startsWith(".") ? filePathOrExt.toLowerCase() : import_node_path.default.extname(filePathOrExt).toLowerCase();
681
+ return MIME_TYPES[ext] ?? "application/octet-stream";
682
+ }
683
+
500
684
  // src/sdk/models/replication.ts
501
685
  var ReplicationMethod = /* @__PURE__ */ ((ReplicationMethod2) => {
502
686
  ReplicationMethod2["INCREMENTAL"] = "INCREMENTAL";
@@ -794,6 +978,9 @@ var Tap = class {
794
978
  this.tapState = (0, import_cloneDeep.default)(initialState.state);
795
979
  }
796
980
  await this.init();
981
+ if (isDryRun()) {
982
+ logger.info(`[DRY_RUN] mode active \u2014 no external writes will occur`);
983
+ }
797
984
  logger.info(`\u{1F680} Start syncing`);
798
985
  const state = StateService.getInstance().get();
799
986
  logger.info(`\u{1F4CD} Received state: %j`, state);
@@ -1819,15 +2006,15 @@ var import_fs_extra = __toESM(require("fs-extra"));
1819
2006
  var import_uniqueId = __toESM(require("lodash/uniqueId.js"));
1820
2007
  var createTemporaryFileStream = (streamId) => {
1821
2008
  import_fs_extra.default.ensureDirSync("./tmp");
1822
- const path2 = `./tmp/${safePath(streamId)}-${(0, import_uniqueId.default)()}.tmp`;
1823
- const writeStream = import_fs_extra.default.createWriteStream(path2, { flags: "w" });
2009
+ const path7 = `./tmp/${safePath(streamId)}-${(0, import_uniqueId.default)()}.tmp`;
2010
+ const writeStream = import_fs_extra.default.createWriteStream(path7, { flags: "w" });
1824
2011
  return {
1825
2012
  stream: writeStream,
1826
- path: path2
2013
+ path: path7
1827
2014
  };
1828
2015
  };
1829
- function safePath(path2) {
1830
- let formattedPath = path2;
2016
+ function safePath(path7) {
2017
+ let formattedPath = path7;
1831
2018
  formattedPath = formattedPath.replace(/^The\s+/gi, "");
1832
2019
  formattedPath = formattedPath.replace(/\s+(?:\(|\[)?Dis(?:c|k) (?:One|Two|Three|Four|Five|Six|Seven|Eight|Nine|Ten|\d)+(?:\)|\])?/gi, "");
1833
2020
  formattedPath = formattedPath.replace(/\s+/gi, "_");
@@ -1953,6 +2140,8 @@ var StreamState = class {
1953
2140
  // src/sdk/models/target/target.ts
1954
2141
  var import_async_mutex = require("async-mutex");
1955
2142
  var import_bluebird3 = __toESM(require("bluebird"));
2143
+ var import_fs_extra2 = __toESM(require("fs-extra"));
2144
+ var import_node_path2 = __toESM(require("path"));
1956
2145
  var semaphore = new import_async_mutex.Semaphore(1);
1957
2146
  var ITarget = class _ITarget {
1958
2147
  config;
@@ -2039,7 +2228,7 @@ var ITarget = class _ITarget {
2039
2228
  if (!stream) {
2040
2229
  throw new MissingSchemaError(`A record for stream:\`${streamId}\` was encountered before a corresponding schema.`, streamId);
2041
2230
  }
2042
- if (stream.getBatchedRowCount() % 1e4 === 0) {
2231
+ if (stream.getBatchedRowCount() % 1e4 === 0 && !isDryRun()) {
2043
2232
  await semaphore.runExclusive(async () => {
2044
2233
  const shouldUploadIntermediateBatch = this.shouldUploadIntermediateBatch(streamId);
2045
2234
  if (shouldUploadIntermediateBatch) {
@@ -2125,7 +2314,9 @@ var ITarget = class _ITarget {
2125
2314
  const { hasBreakingChanges, changes } = this.analyzeSchemaChanges(prevSchema, newFlattenedSchema, streamId);
2126
2315
  if (hasBreakingChanges) {
2127
2316
  logger.info(`\u{1F525} StreamId=${streamId} - Detected BREAKING schema changes: ${changes.join(", ")}. Loading batched records before applying new schema.`);
2128
- await _ITarget.uploadSingleStreamToWarehouse(prevStreamState);
2317
+ if (!isDryRun()) {
2318
+ await _ITarget.uploadSingleStreamToWarehouse(prevStreamState);
2319
+ }
2129
2320
  } else {
2130
2321
  logger.info(`\u2705 StreamId=${streamId} - Detected NON-BREAKING schema changes: ${changes.join(", ")}. Continuing without uploading batched records.`);
2131
2322
  }
@@ -2154,31 +2345,35 @@ var ITarget = class _ITarget {
2154
2345
  Received SCHEMA message: ${JSON.stringify(message)}`);
2155
2346
  }
2156
2347
  streamState.setKeyProperties(message.keyProperties);
2157
- await dbSync.updateSchemaInWarehouse(schema, message.keyProperties);
2158
- const {
2159
- database: databaseName,
2160
- schema: schemaName
2161
- } = this.config;
2162
- const tableName = this.genTableName(streamId);
2163
- const formattedRelationshipMessage = {
2164
- ...message,
2165
- keyProperties: message.keyProperties.map((k) => {
2166
- const translation = streamState.getDbSync().renamedColumnStore.getColumnTranslation(message.stream, k);
2167
- if (translation) {
2168
- return translation;
2169
- }
2170
- return k;
2171
- })
2172
- };
2173
- await import_bluebird3.default.map(this.schemaHooks, async (hook) => {
2174
- const input = {
2175
- databaseName,
2176
- schemaName,
2177
- tableName,
2178
- message: formattedRelationshipMessage
2348
+ if (!isDryRun()) {
2349
+ await dbSync.updateSchemaInWarehouse(schema, message.keyProperties);
2350
+ const {
2351
+ database: databaseName,
2352
+ schema: schemaName
2353
+ } = this.config;
2354
+ const tableName = this.genTableName(streamId);
2355
+ const formattedRelationshipMessage = {
2356
+ ...message,
2357
+ keyProperties: message.keyProperties.map((k) => {
2358
+ const translation = streamState.getDbSync().renamedColumnStore.getColumnTranslation(message.stream, k);
2359
+ if (translation) {
2360
+ return translation;
2361
+ }
2362
+ return k;
2363
+ })
2179
2364
  };
2180
- await hook.writeSchema(input);
2181
- }, { concurrency: 3 });
2365
+ await import_bluebird3.default.map(this.schemaHooks, async (hook) => {
2366
+ const input = {
2367
+ databaseName,
2368
+ schemaName,
2369
+ tableName,
2370
+ message: formattedRelationshipMessage
2371
+ };
2372
+ await hook.writeSchema(input);
2373
+ }, { concurrency: 3 });
2374
+ } else {
2375
+ logger.info(`[DRY_RUN] Stream: ${streamId} - Skipping warehouse schema update and schema hooks`);
2376
+ }
2182
2377
  return Promise.resolve();
2183
2378
  } catch (err) {
2184
2379
  logger.error(`Error when handling schema event.`, err);
@@ -2199,6 +2394,29 @@ var ITarget = class _ITarget {
2199
2394
  };
2200
2395
  complete = async () => {
2201
2396
  try {
2397
+ if (isDryRun()) {
2398
+ logger.info(`[DRY_RUN] Data Extraction is complete. Copying tmp files to out/ instead of loading to warehouse.`);
2399
+ const outDir = import_node_path2.default.resolve("out");
2400
+ await import_fs_extra2.default.ensureDir(outDir);
2401
+ for (const streamId of Object.keys(this.streams)) {
2402
+ const streamState = this.streams[streamId];
2403
+ if (!streamState || streamState.getBatchedRowCount() === 0) continue;
2404
+ const fileToLoad = streamState.getFileToLoad();
2405
+ await new Promise((resolve, reject) => {
2406
+ fileToLoad.stream.end(() => {
2407
+ fileToLoad.stream.close((err) => {
2408
+ if (err) reject(err);
2409
+ else resolve();
2410
+ });
2411
+ });
2412
+ });
2413
+ const destPath = import_node_path2.default.join(outDir, `${safePath(streamId)}.ndjson`);
2414
+ await import_fs_extra2.default.copy(fileToLoad.path, destPath);
2415
+ logger.info(`[DRY_RUN] Stream: ${streamId} - Copied ${streamState.getBatchedRowCount()} records to ${destPath}`);
2416
+ await import_fs_extra2.default.remove(fileToLoad.path);
2417
+ }
2418
+ return;
2419
+ }
2202
2420
  logger.info(`\u{1F44D} Data Extraction is complete. Will load remaining batched stream records.`);
2203
2421
  await this.loadAllStreamsInWarehouse({ isFinalLoad: true });
2204
2422
  return Promise.resolve();
@@ -2631,7 +2849,7 @@ var formatHexComparison = (expected, actual) => {
2631
2849
  Expected: "${expected}" (${expectedInfo.length} chars) [${expectedInfo.hex}]
2632
2850
  Actual: "${actual}" (${actualInfo.length} chars) [${actualInfo.hex}]`;
2633
2851
  };
2634
- async function* rowGeneratorFromCsv(path2, fileConfig) {
2852
+ async function* rowGeneratorFromCsv(path7, fileConfig) {
2635
2853
  const isGeneratorConfigArray = Array.isArray(fileConfig.fields);
2636
2854
  const configKeys = isGeneratorConfigArray ? [] : Object.keys(fileConfig.fields);
2637
2855
  const csvOptions = { separator: fileConfig.separator };
@@ -2643,10 +2861,10 @@ async function* rowGeneratorFromCsv(path2, fileConfig) {
2643
2861
  let finalInputStream;
2644
2862
  const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
2645
2863
  if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
2646
- initialReadStream = (0, import_fs2.createReadStream)(path2);
2864
+ initialReadStream = (0, import_fs2.createReadStream)(path7);
2647
2865
  finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
2648
2866
  } else {
2649
- initialReadStream = (0, import_fs2.createReadStream)(path2);
2867
+ initialReadStream = (0, import_fs2.createReadStream)(path7);
2650
2868
  finalInputStream = initialReadStream.pipe(createStripBomTransform());
2651
2869
  }
2652
2870
  finalInputStream.pipe(csvStream);
@@ -2688,7 +2906,7 @@ async function countCsvLines(filePath) {
2688
2906
  }).on("end", () => resolve(Math.max(0, newlines - 1))).on("error", reject);
2689
2907
  });
2690
2908
  }
2691
- var checkCsvHeaderRow = async (path2, fileConfig) => {
2909
+ var checkCsvHeaderRow = async (path7, fileConfig) => {
2692
2910
  return new Promise((resolve, reject) => {
2693
2911
  const colsWithParsingConfig = Object.keys(fileConfig.fields).map((key) => key.trim());
2694
2912
  console.debug(`${logPrefix} CSV Expected columns:`, colsWithParsingConfig.map((col) => {
@@ -2705,10 +2923,10 @@ var checkCsvHeaderRow = async (path2, fileConfig) => {
2705
2923
  let finalInputStream;
2706
2924
  const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
2707
2925
  if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
2708
- initialReadStream = (0, import_fs2.createReadStream)(path2);
2926
+ initialReadStream = (0, import_fs2.createReadStream)(path7);
2709
2927
  finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
2710
2928
  } else {
2711
- initialReadStream = (0, import_fs2.createReadStream)(path2);
2929
+ initialReadStream = (0, import_fs2.createReadStream)(path7);
2712
2930
  finalInputStream = initialReadStream.pipe(createStripBomTransform());
2713
2931
  }
2714
2932
  let isFirstLine = true;
@@ -2734,7 +2952,7 @@ var checkCsvHeaderRow = async (path2, fileConfig) => {
2734
2952
  i,
2735
2953
  col,
2736
2954
  headers[i] || "<undefined>",
2737
- path2
2955
+ path7
2738
2956
  ));
2739
2957
  }
2740
2958
  });
@@ -2751,7 +2969,7 @@ var checkCsvHeaderRow = async (path2, fileConfig) => {
2751
2969
  throw new Error((0, import_util3.format)(
2752
2970
  `CSV missing expected column '%s' in file: %s`,
2753
2971
  col,
2754
- path2
2972
+ path7
2755
2973
  ));
2756
2974
  }
2757
2975
  });
@@ -2777,7 +2995,7 @@ var writeDataToCsv = async (fileName, data) => {
2777
2995
  try {
2778
2996
  const firstRow = data[0];
2779
2997
  if (!firstRow) {
2780
- console.log(`${logPrefix2} No data to write to CSV file=%s`, fileName);
2998
+ logger.info(`${logPrefix2} No data to write to CSV file=%s`, fileName);
2781
2999
  return;
2782
3000
  }
2783
3001
  const csvWriter = (0, import_csv_writer.createObjectCsvWriter)({
@@ -2787,7 +3005,7 @@ var writeDataToCsv = async (fileName, data) => {
2787
3005
  })
2788
3006
  });
2789
3007
  await csvWriter.writeRecords(data);
2790
- console.log(`${logPrefix2} CSV file=%s written successfully`, fileName);
3008
+ logger.info(`${logPrefix2} CSV file=%s written successfully`, fileName);
2791
3009
  } catch (err) {
2792
3010
  if (err instanceof Error) {
2793
3011
  throw new Error((0, import_util4.format)(`error while writing csv file=%s, err: %s`, fileName, err.message));
@@ -2795,15 +3013,17 @@ var writeDataToCsv = async (fileName, data) => {
2795
3013
  throw err;
2796
3014
  }
2797
3015
  };
2798
- var writeGeneratorToCSV = async (generator, outputFileName) => {
3016
+ var writeGeneratorToCSV = async (generator, outputFileName, options) => {
3017
+ const batchSize = options?.batchSize ?? 1e4;
3018
+ const alwaysQuote = options?.alwaysQuote ?? true;
2799
3019
  try {
2800
3020
  let rowCount = 0;
2801
3021
  const firstDataRow = (await generator.next()).value;
2802
3022
  if (!firstDataRow) {
2803
- console.log(`${logPrefix2} [%s] first data row of csv empty, skipping`, outputFileName);
3023
+ logger.info(`${logPrefix2} [%s] first data row of csv empty, skipping`, outputFileName);
2804
3024
  return 0;
2805
3025
  }
2806
- console.log(`${logPrefix2} [%s] inferring headers from first data row`, outputFileName);
3026
+ logger.info(`${logPrefix2} [%s] inferring headers from first data row`, outputFileName);
2807
3027
  const headers = Object.keys(firstDataRow).map((col) => {
2808
3028
  return {
2809
3029
  id: col,
@@ -2813,14 +3033,14 @@ var writeGeneratorToCSV = async (generator, outputFileName) => {
2813
3033
  const csvWriter = (0, import_csv_writer.createObjectCsvWriter)({
2814
3034
  path: outputFileName,
2815
3035
  header: headers,
2816
- alwaysQuote: true
3036
+ alwaysQuote
2817
3037
  });
2818
- console.log(`${logPrefix2} [%s] writing csv`, outputFileName);
3038
+ logger.info(`${logPrefix2} [%s] writing csv`, outputFileName);
2819
3039
  let batch = [firstDataRow];
2820
3040
  for await (const data of generator) {
2821
3041
  batch.push(data);
2822
3042
  rowCount++;
2823
- if (batch.length > 1e4) {
3043
+ if (batch.length >= batchSize) {
2824
3044
  try {
2825
3045
  await csvWriter.writeRecords(batch);
2826
3046
  } catch (err) {
@@ -2838,8 +3058,10 @@ var writeGeneratorToCSV = async (generator, outputFileName) => {
2838
3058
  batch = [];
2839
3059
  }
2840
3060
  }
2841
- await csvWriter.writeRecords(batch);
2842
- console.log(`${logPrefix2} [%s] csv written successfully with %d rows`, outputFileName, rowCount);
3061
+ if (batch.length > 0) {
3062
+ await csvWriter.writeRecords(batch);
3063
+ }
3064
+ logger.info(`${logPrefix2} [%s] csv written successfully with %d rows`, outputFileName, rowCount);
2843
3065
  return rowCount;
2844
3066
  } catch (err) {
2845
3067
  if (err instanceof Error) {
@@ -3125,9 +3347,65 @@ var FilePatterns = class {
3125
3347
  };
3126
3348
 
3127
3349
  // src/services/sftp.ts
3350
+ var import_node_path3 = __toESM(require("path"));
3351
+ var import_fs_extra3 = __toESM(require("fs-extra"));
3128
3352
  var import_ssh2_sftp_client = __toESM(require("ssh2-sftp-client"));
3129
3353
  var import_ssh2_sftp_client2 = __toESM(require("ssh2-sftp-client"));
3130
3354
  var SftpService = new import_ssh2_sftp_client.default();
3355
+ var logPrefix3 = "[sftp]";
3356
+ async function listFilesRecursively(client, remotePath, options) {
3357
+ const recursive = options?.recursive ?? true;
3358
+ const extensions = options?.extensions?.map((e) => e.toLowerCase());
3359
+ const maxIterations = options?.maxIterations ?? 1e3;
3360
+ const results = [];
3361
+ const dirs = [remotePath];
3362
+ let iterations = 0;
3363
+ while (dirs.length > 0) {
3364
+ if (iterations++ >= maxIterations) {
3365
+ logger.warn(`${logPrefix3} listFilesRecursively reached maxIterations (${maxIterations}), stopping.`);
3366
+ break;
3367
+ }
3368
+ const currentDir = dirs.shift();
3369
+ let entries;
3370
+ try {
3371
+ entries = await client.list(currentDir);
3372
+ } catch (err) {
3373
+ const msg = err instanceof Error ? err.message : String(err);
3374
+ logger.warn(`${logPrefix3} Failed to list ${currentDir}: ${msg}`);
3375
+ continue;
3376
+ }
3377
+ for (const entry of entries) {
3378
+ if (entry.type === "d") {
3379
+ if (recursive && entry.name !== "." && entry.name !== "..") {
3380
+ dirs.push(import_node_path3.default.posix.join(currentDir, entry.name));
3381
+ }
3382
+ continue;
3383
+ }
3384
+ if (entry.type !== "-") continue;
3385
+ if (extensions) {
3386
+ const ext = import_node_path3.default.extname(entry.name).toLowerCase();
3387
+ if (!extensions.includes(ext)) continue;
3388
+ }
3389
+ const fullPath = import_node_path3.default.posix.join(currentDir, entry.name);
3390
+ results.push({ ...entry, name: fullPath });
3391
+ }
3392
+ }
3393
+ logger.info(`${logPrefix3} listFilesRecursively found ${results.length} files under ${remotePath}`);
3394
+ return results;
3395
+ }
3396
+ async function downloadFiles(client, files, localDir, options) {
3397
+ const concurrency = options?.concurrency ?? 5;
3398
+ await import_fs_extra3.default.ensureDir(localDir);
3399
+ const tasks = files.map((file, index) => () => {
3400
+ const localName = `${index}_${import_node_path3.default.basename(file.name)}`;
3401
+ const localPath = import_node_path3.default.join(localDir, localName);
3402
+ logger.debug(`${logPrefix3} Downloading ${file.name} \u2192 ${localPath}`);
3403
+ return client.fastGet(file.name, localPath).then(() => localPath);
3404
+ });
3405
+ const results = await runWithConcurrency(tasks, concurrency);
3406
+ logger.info(`${logPrefix3} Downloaded ${results.length} files to ${localDir}`);
3407
+ return results;
3408
+ }
3131
3409
 
3132
3410
  // src/services/cloud-storage.ts
3133
3411
  var import_storage = require("@google-cloud/storage");
@@ -3135,7 +3413,7 @@ var import_util5 = require("util");
3135
3413
  var pathModule = __toESM(require("path"));
3136
3414
  var import_fs3 = require("fs");
3137
3415
  var import_crypto = require("crypto");
3138
- var logPrefix3 = "[CloudStorageService]";
3416
+ var logPrefix4 = "[CloudStorageService]";
3139
3417
  var tmpDir = "tmp";
3140
3418
  var CloudStorageService = class {
3141
3419
  storage;
@@ -3143,10 +3421,10 @@ var CloudStorageService = class {
3143
3421
  processedSuffix;
3144
3422
  supportedExtensions;
3145
3423
  path;
3146
- constructor(bucketName, path2, opts) {
3424
+ constructor(bucketName, path7, opts) {
3147
3425
  this.storage = new import_storage.Storage({ retryOptions: { autoRetry: true, maxRetries: 20 } });
3148
3426
  this.bucket = this.storage.bucket(bucketName);
3149
- this.path = path2;
3427
+ this.path = path7;
3150
3428
  this.processedSuffix = opts?.processedSuffix ?? ".processed";
3151
3429
  this.supportedExtensions = opts?.supportedExtensions ?? [];
3152
3430
  }
@@ -3187,7 +3465,7 @@ var CloudStorageService = class {
3187
3465
  try {
3188
3466
  const file = this.bucket.file(markerFileName);
3189
3467
  await file.save((0, import_util5.format)("Marked file %s as processed", fileName));
3190
- logger.info(`${logPrefix3} Marker file ${markerFileName} created successfully.`);
3468
+ logger.info(`${logPrefix4} Marker file ${markerFileName} created successfully.`);
3191
3469
  } catch (err) {
3192
3470
  if (err instanceof Error) {
3193
3471
  throw new Error((0, import_util5.format)(`error while writing marker file=%s, err:%s`, markerFileName, err.message));
@@ -3211,7 +3489,7 @@ var CloudStorageService = class {
3211
3489
  (0, import_fs3.unlinkSync)(destFilename);
3212
3490
  }
3213
3491
  await file.download({ destination: destFilename });
3214
- logger.info(`${logPrefix3} Downloaded ${fileName} to ${destFilename}`);
3492
+ logger.info(`${logPrefix4} Downloaded ${fileName} to ${destFilename}`);
3215
3493
  return destFilename;
3216
3494
  } catch (err) {
3217
3495
  if (err instanceof Error) {
@@ -3229,9 +3507,9 @@ var CloudStorageService = class {
3229
3507
  */
3230
3508
  async uploadFile(localPath, destPath) {
3231
3509
  try {
3232
- logger.info(`${logPrefix3} preparing to upload '%s' to '%s'`, localPath, destPath);
3510
+ logger.info(`${logPrefix4} preparing to upload '%s' to '%s'`, localPath, destPath);
3233
3511
  const [file] = await this.bucket.upload(localPath, { destination: destPath });
3234
- logger.info(`${logPrefix3} file %s has been successfully uploaded into %s`, localPath, destPath);
3512
+ logger.info(`${logPrefix4} file %s has been successfully uploaded into %s`, localPath, destPath);
3235
3513
  return file;
3236
3514
  } catch (err) {
3237
3515
  if (err instanceof Error) {
@@ -3307,6 +3585,376 @@ var unzip = async (zipFilePath, extractedPath) => {
3307
3585
  return extractedPath;
3308
3586
  };
3309
3587
 
3588
+ // src/services/cdn.ts
3589
+ var import_axios3 = __toESM(require("axios"));
3590
+ var import_axios_retry2 = __toESM(require("axios-retry"));
3591
+ var logPrefix5 = "[CdnService]";
3592
+ var MAX_RETRIES = 10;
3593
+ var CdnService = class {
3594
+ axiosClient;
3595
+ constructor(config) {
3596
+ const resolvedKey = getServiceAccountKey(config.serviceAccountKey);
3597
+ const resolvedEndpoint = getApiEndpoint(config.apiEndpoint);
3598
+ this.axiosClient = import_axios3.default.create({
3599
+ baseURL: resolvedEndpoint,
3600
+ timeout: 12e4,
3601
+ headers: {
3602
+ Authorization: `Bearer ${resolvedKey}`,
3603
+ Accept: "application/json"
3604
+ }
3605
+ });
3606
+ (0, import_axios_retry2.default)(this.axiosClient, {
3607
+ retries: MAX_RETRIES,
3608
+ retryCondition: (error) => {
3609
+ const status = error.response?.status ?? 0;
3610
+ return !error.response || status === 429 || status >= 500 && status < 600;
3611
+ },
3612
+ retryDelay: (retryCount, error) => {
3613
+ const retryAfter = error.response?.headers?.["retry-after"];
3614
+ if (retryAfter) {
3615
+ const seconds = Number(retryAfter);
3616
+ if (!isNaN(seconds) && seconds > 0) {
3617
+ logger.info(`${logPrefix5} Rate limited \u2014 waiting ${seconds}s (Retry-After header), attempt ${retryCount}/${MAX_RETRIES}`);
3618
+ return seconds * 1e3;
3619
+ }
3620
+ }
3621
+ const delay = import_axios_retry2.default.exponentialDelay(retryCount);
3622
+ logger.info(`${logPrefix5} Retrying in ${delay}ms (attempt ${retryCount}/${MAX_RETRIES}) for ${error.config?.method?.toUpperCase()} ${error.config?.url}`);
3623
+ return delay;
3624
+ }
3625
+ });
3626
+ }
3627
+ /**
3628
+ * Returns metadata for a CDN file via a HEAD request.
3629
+ * If the file does not exist, returns `{ exists: false, lastModified: null, contentType: null }`.
3630
+ * Falls back to not-existing if HEAD is not supported (405).
3631
+ */
3632
+ async getFileMetadata(cdnId, fileName) {
3633
+ const url = `/v1/cdn/${cdnId}/files/${encodeURIComponent(fileName)}`;
3634
+ try {
3635
+ const response = await this.axiosClient.head(url);
3636
+ const lastModifiedHeader = response.headers["last-modified"];
3637
+ const contentTypeHeader = response.headers["content-type"];
3638
+ return {
3639
+ exists: true,
3640
+ lastModified: lastModifiedHeader ? new Date(lastModifiedHeader) : null,
3641
+ contentType: contentTypeHeader ?? null
3642
+ };
3643
+ } catch (err) {
3644
+ if (import_axios3.default.isAxiosError(err)) {
3645
+ const status = err.response?.status;
3646
+ if (status === 404) return { exists: false, lastModified: null, contentType: null };
3647
+ if (status === 405) {
3648
+ logger.warn(`${logPrefix5} HEAD not supported for ${url}, treating file as absent`);
3649
+ return { exists: false, lastModified: null, contentType: null };
3650
+ }
3651
+ this.throwMeaningfulError(err);
3652
+ }
3653
+ throw err;
3654
+ }
3655
+ }
3656
+ /**
3657
+ * Checks whether a file already exists in the CDN via a HEAD request.
3658
+ * Returns true if the server responds 2xx, false on 404.
3659
+ * Falls back to false (treat as not existing) if HEAD is not supported (405).
3660
+ * No body is transferred.
3661
+ */
3662
+ async fileExists(cdnId, fileName) {
3663
+ return (await this.getFileMetadata(cdnId, fileName)).exists;
3664
+ }
3665
+ /**
3666
+ * Uploads a file buffer to the Whaly CDN.
3667
+ * The PUT is idempotent — uploading the same fileName overwrites the existing file.
3668
+ */
3669
+ async uploadFile(cdnId, fileName, fileBuffer) {
3670
+ const url = `/v1/cdn/${cdnId}/files/${encodeURIComponent(fileName)}`;
3671
+ try {
3672
+ logger.info(`${logPrefix5} Uploading ${fileName} (${fileBuffer.length} bytes) to CDN ${cdnId}`);
3673
+ const form = new FormData();
3674
+ form.append("file", new Blob([fileBuffer]), fileName);
3675
+ const response = await this.axiosClient.put(url, form);
3676
+ logger.info(`${logPrefix5} Successfully uploaded ${fileName} (status ${response.status})`);
3677
+ return {
3678
+ filePath: response.data?.filePath ?? `/org/${cdnId}/file/${fileName}`
3679
+ };
3680
+ } catch (err) {
3681
+ if (import_axios3.default.isAxiosError(err)) {
3682
+ this.throwMeaningfulError(err);
3683
+ }
3684
+ throw err;
3685
+ }
3686
+ }
3687
+ throwMeaningfulError(err) {
3688
+ const status = err.response?.status;
3689
+ if (status === 401 || status === 403) {
3690
+ throw new Error("Unauthorized - invalid or expired service account key");
3691
+ } else if (status === 404) {
3692
+ throw new Error("Organization or CDN not found");
3693
+ } else if (status === 500) {
3694
+ throw new Error("API server error - check CDN ID and service account permissions");
3695
+ } else {
3696
+ throw new Error(`API error: ${err.response?.statusText ?? err.message}`);
3697
+ }
3698
+ }
3699
+ };
3700
+
3701
+ // src/sdk/models/asset-tap/asset-stream.ts
3702
+ var AssetStream = class {
3703
+ streamId;
3704
+ config;
3705
+ replicationMode;
3706
+ constructor(streamId, config, replicationMode = "INCREMENTAL") {
3707
+ this.streamId = streamId;
3708
+ this.config = config;
3709
+ this.replicationMode = replicationMode;
3710
+ }
3711
+ async transformFile(downloadedPath, _entry) {
3712
+ return downloadedPath;
3713
+ }
3714
+ };
3715
+
3716
+ // src/sdk/models/asset-tap/asset-tap.ts
3717
+ var import_fs_extra4 = __toESM(require("fs-extra"));
3718
+ var import_node_path4 = __toESM(require("path"));
3719
+ var logPrefix6 = "[AssetTap]";
3720
+ function inferContentType(filePath, fallback) {
3721
+ const mime = getMimeType(filePath);
3722
+ return mime !== "application/octet-stream" ? mime : fallback;
3723
+ }
3724
+ function deriveManifestMode(streams) {
3725
+ const modes = new Set(streams.map((s) => s.replicationMode));
3726
+ if (modes.size > 1) {
3727
+ logger.warn(`${logPrefix6} Streams have mixed replication modes (${[...modes].join(", ")}). Manifest will report "FULL".`);
3728
+ return "FULL";
3729
+ }
3730
+ return streams[0]?.replicationMode ?? "INCREMENTAL";
3731
+ }
3732
+ var AssetTap = class {
3733
+ config;
3734
+ outputDir;
3735
+ streams = [];
3736
+ concurrency;
3737
+ target;
3738
+ constructor(target, config, outputDir = "out", concurrency = 5) {
3739
+ this.target = target;
3740
+ this.config = config;
3741
+ this.outputDir = outputDir;
3742
+ this.concurrency = concurrency;
3743
+ }
3744
+ async sync() {
3745
+ await this.init();
3746
+ const dryRun = isDryRun();
3747
+ const dryRunLimit = dryRun ? getDryRunLimit() : void 0;
3748
+ if (dryRun) {
3749
+ logger.info(`${logPrefix6} [DRY_RUN] mode active \u2014 skipping CDN checks and uploads`);
3750
+ await import_fs_extra4.default.emptyDir(this.outputDir);
3751
+ if (dryRunLimit !== void 0) {
3752
+ logger.info(`${logPrefix6} [DRY_RUN] Limit: ${dryRunLimit} assets per stream`);
3753
+ }
3754
+ } else if (process.env["DRY_RUN_LIMIT"] !== void 0) {
3755
+ logger.warn(`${logPrefix6} DRY_RUN_LIMIT is set but DRY_RUN is not active \u2014 limit will be ignored`);
3756
+ }
3757
+ const tmpDir2 = import_node_path4.default.join(this.outputDir, "tmp");
3758
+ await import_fs_extra4.default.ensureDir(tmpDir2);
3759
+ const streamManifests = [];
3760
+ let entryIndex = 0;
3761
+ let totalSummary = { total: 0, uploaded: 0, skipped: 0, errors: 0 };
3762
+ for (const stream of this.streams) {
3763
+ logger.info(`${logPrefix6} Processing stream: ${stream.streamId} (mode=${stream.replicationMode}, concurrency=${this.concurrency})`);
3764
+ const assetEntries = [];
3765
+ let streamAssetCount = 0;
3766
+ await processFromAsyncIterable(
3767
+ stream.listAssets(),
3768
+ async (entry) => {
3769
+ if (dryRunLimit !== void 0 && streamAssetCount >= dryRunLimit) {
3770
+ return "stop";
3771
+ }
3772
+ logger.debug(`${logPrefix6} Processing entry: ${entry.sourcePath}`);
3773
+ if (stream.replicationMode === "INCREMENTAL" && !dryRun) {
3774
+ const shouldSync = await this.target.shouldSync(entry);
3775
+ if (!shouldSync) {
3776
+ logger.debug(`${logPrefix6} Skipping ${entry.sourcePath} (up-to-date)`);
3777
+ assetEntries.push({
3778
+ sourcePath: entry.sourcePath,
3779
+ destinationPath: entry.destinationPath,
3780
+ downloadedPath: "",
3781
+ transformedPath: "",
3782
+ size: 0,
3783
+ contentType: entry.contentType,
3784
+ status: "skipped",
3785
+ transformed: false
3786
+ });
3787
+ return "continue";
3788
+ }
3789
+ }
3790
+ const fileName = `${entryIndex}_${import_node_path4.default.basename(entry.sourcePath)}`;
3791
+ const downloadedPath = import_node_path4.default.join(tmpDir2, fileName);
3792
+ let uploadPath = downloadedPath;
3793
+ entryIndex++;
3794
+ streamAssetCount++;
3795
+ try {
3796
+ await stream.downloadEntry(entry, downloadedPath);
3797
+ uploadPath = await stream.transformFile(downloadedPath, entry);
3798
+ const wasTransformed = uploadPath !== downloadedPath;
3799
+ const stat = await import_fs_extra4.default.stat(uploadPath);
3800
+ const processed = {
3801
+ entry,
3802
+ downloadedPath,
3803
+ uploadPath,
3804
+ wasTransformed,
3805
+ size: stat.size,
3806
+ contentType: wasTransformed ? inferContentType(uploadPath, entry.contentType) : entry.contentType
3807
+ };
3808
+ if (!dryRun) {
3809
+ await this.target.uploadAsset(processed);
3810
+ } else {
3811
+ const inspectPath = import_node_path4.default.join(this.outputDir, stream.streamId, entry.destinationPath);
3812
+ await import_fs_extra4.default.ensureDir(import_node_path4.default.dirname(inspectPath));
3813
+ await import_fs_extra4.default.copy(uploadPath, inspectPath);
3814
+ }
3815
+ assetEntries.push({
3816
+ sourcePath: entry.sourcePath,
3817
+ destinationPath: entry.destinationPath,
3818
+ downloadedPath,
3819
+ transformedPath: wasTransformed ? uploadPath : "",
3820
+ size: processed.size,
3821
+ contentType: processed.contentType,
3822
+ status: "uploaded",
3823
+ transformed: wasTransformed
3824
+ });
3825
+ logger.info(`${logPrefix6} ${dryRun ? "[DRY_RUN] Processed" : "Uploaded"} ${entry.sourcePath} \u2192 ${entry.destinationPath}`);
3826
+ } catch (err) {
3827
+ const message = err instanceof Error ? err.message : String(err);
3828
+ logger.error(`${logPrefix6} Failed to process ${entry.sourcePath}: ${message}`);
3829
+ assetEntries.push({
3830
+ sourcePath: entry.sourcePath,
3831
+ destinationPath: entry.destinationPath,
3832
+ downloadedPath: "",
3833
+ transformedPath: "",
3834
+ size: 0,
3835
+ contentType: entry.contentType,
3836
+ status: "error",
3837
+ transformed: false,
3838
+ error: message
3839
+ });
3840
+ } finally {
3841
+ await import_fs_extra4.default.remove(downloadedPath).catch(() => void 0);
3842
+ if (uploadPath !== downloadedPath) {
3843
+ await import_fs_extra4.default.remove(uploadPath).catch(() => void 0);
3844
+ }
3845
+ }
3846
+ if (dryRunLimit !== void 0 && streamAssetCount >= dryRunLimit) {
3847
+ logger.info(`${logPrefix6} [DRY_RUN] Reached limit of ${dryRunLimit} for stream "${stream.streamId}", stopping.`);
3848
+ return "stop";
3849
+ }
3850
+ return "continue";
3851
+ },
3852
+ this.concurrency
3853
+ );
3854
+ const streamSummary = {
3855
+ total: assetEntries.length,
3856
+ uploaded: assetEntries.filter((a) => a.status === "uploaded").length,
3857
+ skipped: assetEntries.filter((a) => a.status === "skipped").length,
3858
+ errors: assetEntries.filter((a) => a.status === "error").length
3859
+ };
3860
+ totalSummary.total += streamSummary.total;
3861
+ totalSummary.uploaded += streamSummary.uploaded;
3862
+ totalSummary.skipped += streamSummary.skipped;
3863
+ totalSummary.errors += streamSummary.errors;
3864
+ streamManifests.push({
3865
+ streamId: stream.streamId,
3866
+ mode: stream.replicationMode,
3867
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
3868
+ assets: assetEntries,
3869
+ summary: streamSummary
3870
+ });
3871
+ }
3872
+ const manifest = {
3873
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
3874
+ mode: deriveManifestMode(this.streams),
3875
+ streams: streamManifests,
3876
+ summary: totalSummary
3877
+ };
3878
+ await import_fs_extra4.default.ensureDir(this.outputDir);
3879
+ await import_fs_extra4.default.writeJson(import_node_path4.default.join(this.outputDir, "manifest.json"), manifest, { spaces: 2 });
3880
+ if (!dryRun) {
3881
+ await this.target.complete();
3882
+ }
3883
+ logger.info(`${logPrefix6} Sync complete. Uploaded=${totalSummary.uploaded} Skipped=${totalSummary.skipped} Errors=${totalSummary.errors}`);
3884
+ return manifest;
3885
+ }
3886
+ };
3887
+
3888
+ // src/sdk/models/asset-tap/image-transform.ts
3889
+ var import_sharp = __toESM(require("sharp"));
3890
+ var import_node_path5 = __toESM(require("path"));
3891
+ var SHARP_GRAVITY = {
3892
+ center: "centre",
3893
+ top: "north",
3894
+ bottom: "south",
3895
+ left: "west",
3896
+ right: "east",
3897
+ "top left": "northwest",
3898
+ "top right": "northeast",
3899
+ "bottom left": "southwest",
3900
+ "bottom right": "southeast"
3901
+ };
3902
+ function parseBackground(bg) {
3903
+ if (bg === void 0 || bg === "none" || bg === "transparent") {
3904
+ return { r: 0, g: 0, b: 0, alpha: 0 };
3905
+ }
3906
+ return bg;
3907
+ }
3908
+ var ImageTransform = class {
3909
+ /**
3910
+ * Converts an image to WebP format using sharp (libvips).
3911
+ *
3912
+ * The output file is written to the same directory as the input,
3913
+ * with the extension replaced by `.webp`.
3914
+ *
3915
+ * @param inputPath Path to the source image file.
3916
+ * @param options Optional resize / padding parameters.
3917
+ * @returns Path to the produced `.webp` file.
3918
+ */
3919
+ static async toWebp(inputPath, options) {
3920
+ const dir = import_node_path5.default.dirname(inputPath);
3921
+ const basename = import_node_path5.default.basename(inputPath, import_node_path5.default.extname(inputPath));
3922
+ const outputPath = import_node_path5.default.join(dir, `${basename}.webp`);
3923
+ let pipeline = (0, import_sharp.default)(inputPath);
3924
+ const hasSize = options.width !== void 0 && options.height !== void 0;
3925
+ if (hasSize) {
3926
+ const position = options.gravity !== void 0 ? SHARP_GRAVITY[options.gravity] ?? options.gravity : void 0;
3927
+ if (options.extent === true) {
3928
+ pipeline = pipeline.resize(options.width, options.height, {
3929
+ fit: "contain",
3930
+ position,
3931
+ background: parseBackground(options.background)
3932
+ });
3933
+ } else {
3934
+ pipeline = pipeline.resize(options.width, options.height, {
3935
+ fit: "inside",
3936
+ position
3937
+ });
3938
+ }
3939
+ }
3940
+ pipeline = pipeline.webp(
3941
+ options.quality !== void 0 ? { quality: options.quality } : {}
3942
+ );
3943
+ await pipeline.toFile(outputPath);
3944
+ return outputPath;
3945
+ }
3946
+ };
3947
+
3948
+ // src/sdk/models/asset-target/asset-target.ts
3949
+ var AssetTarget = class {
3950
+ config;
3951
+ constructor(config) {
3952
+ this.config = config;
3953
+ }
3954
+ async complete() {
3955
+ }
3956
+ };
3957
+
3310
3958
  // src/targets/bigquery/helpers.ts
3311
3959
  var safeColumnName = (key) => {
3312
3960
  let returnString = key;
@@ -3343,7 +3991,9 @@ var import_decimal = __toESM(require("decimal.js"));
3343
3991
  var import_dayjs5 = __toESM(require("dayjs"));
3344
3992
  var validateDateRange = (record, schema) => {
3345
3993
  Object.keys(schema).forEach((key) => {
3346
- const { format: format8 } = schema[key];
3994
+ const field = schema[key];
3995
+ if (!field) return;
3996
+ const { format: format8 } = field;
3347
3997
  if (format8 === "date-time") {
3348
3998
  if (record[key]) {
3349
3999
  const formattedDate = (0, import_dayjs5.default)(record[key]).format(defaultDateTimeFormat);
@@ -3860,6 +4510,46 @@ var BigQueryTarget = class extends ITarget {
3860
4510
  convertNumberIntoDecimal = convertNumberIntoDecimal;
3861
4511
  };
3862
4512
 
4513
+ // src/targets/cdn/main.ts
4514
+ var import_fs_extra5 = __toESM(require("fs-extra"));
4515
+ var logPrefix7 = "[CdnAssetTarget]";
4516
+ var CdnAssetTarget = class extends AssetTarget {
4517
+ cdnService;
4518
+ constructor(config) {
4519
+ super(config);
4520
+ this.cdnService = new CdnService(config);
4521
+ }
4522
+ async shouldSync(entry) {
4523
+ const metadata = await this.cdnService.getFileMetadata(getCdnId(this.config.cdnId), entry.destinationPath);
4524
+ if (!metadata.exists) {
4525
+ logger.debug(`${logPrefix7} ${entry.destinationPath} not in CDN \u2192 will sync`);
4526
+ return true;
4527
+ }
4528
+ if (entry.lastModified === void 0) {
4529
+ logger.debug(`${logPrefix7} ${entry.destinationPath} source has no lastModified \u2192 will sync`);
4530
+ return true;
4531
+ }
4532
+ if (metadata.lastModified === null) {
4533
+ logger.debug(`${logPrefix7} ${entry.destinationPath} CDN has no lastModified \u2192 will sync`);
4534
+ return true;
4535
+ }
4536
+ const sourceIsNewer = entry.lastModified > metadata.lastModified;
4537
+ if (!sourceIsNewer) {
4538
+ logger.debug(`${logPrefix7} Skipping ${entry.destinationPath} (CDN is up-to-date)`);
4539
+ }
4540
+ return sourceIsNewer;
4541
+ }
4542
+ async uploadAsset(asset) {
4543
+ const fileBuffer = await import_fs_extra5.default.readFile(asset.uploadPath);
4544
+ await this.cdnService.uploadFile(
4545
+ getCdnId(this.config.cdnId),
4546
+ asset.entry.destinationPath,
4547
+ fileBuffer
4548
+ );
4549
+ logger.info(`${logPrefix7} Uploaded ${asset.entry.destinationPath} (${fileBuffer.length} bytes)`);
4550
+ }
4551
+ };
4552
+
3863
4553
  // src/state-providers/gcs/main.ts
3864
4554
  var import_util6 = require("util");
3865
4555
  var GCSStateProvider = class {
@@ -3901,9 +4591,14 @@ var GCSStateProvider = class {
3901
4591
  0 && (module.exports = {
3902
4592
  ALL_STREAM_ID_KEYWORD,
3903
4593
  API_CALLS_METRIC_NAME,
4594
+ AssetStream,
4595
+ AssetTap,
4596
+ AssetTarget,
3904
4597
  Authenticator,
3905
4598
  BATCH_INTERVAL_MS,
3906
4599
  BigQueryTarget,
4600
+ CdnAssetTarget,
4601
+ CdnService,
3907
4602
  CloudStorageService,
3908
4603
  CounterMetric,
3909
4604
  CsvExtractionConfigBuilder,
@@ -3917,6 +4612,8 @@ var GCSStateProvider = class {
3917
4612
  FileTap,
3918
4613
  GCSStateProvider,
3919
4614
  ITarget,
4615
+ ImageTransform,
4616
+ MIME_TYPES,
3920
4617
  MissingFieldInSchemaError,
3921
4618
  MissingSchemaError,
3922
4619
  RESTStream,
@@ -3938,6 +4635,7 @@ var GCSStateProvider = class {
3938
4635
  _greaterThan,
3939
4636
  addWhalyFields,
3940
4637
  checkCsvHeaderRow,
4638
+ collectPaginated,
3941
4639
  countCsvLines,
3942
4640
  createCellReference,
3943
4641
  createCsvStreamConfig,
@@ -3945,6 +4643,7 @@ var GCSStateProvider = class {
3945
4643
  createExcelStreamConfig,
3946
4644
  createTemporaryFileStream,
3947
4645
  csvFieldsToJsonSchema,
4646
+ downloadFiles,
3948
4647
  excelColumnToIndex,
3949
4648
  excelFieldsToJsonSchema,
3950
4649
  excelRowToIndex,
@@ -3957,29 +4656,40 @@ var GCSStateProvider = class {
3957
4656
  findAllMatchingConfigs,
3958
4657
  flattenSchema,
3959
4658
  getAllMetrics,
4659
+ getApiEndpoint,
3960
4660
  getAxiosInstance,
4661
+ getCdnId,
3961
4662
  getCounterMetric,
3962
4663
  getCounterMetrics,
3963
4664
  getDownloadFileApiCall,
4665
+ getDryRunLimit,
3964
4666
  getJSONApiCall,
3965
4667
  getJSONApiCallWithFullResponse,
4668
+ getMimeType,
3966
4669
  getRawJSONApiCall,
4670
+ getServiceAccountKey,
3967
4671
  gracefulExit,
3968
4672
  haltAndCatchFire,
3969
4673
  incrementStreamState,
3970
4674
  indexToExcelColumn,
3971
4675
  indexToExcelRow,
4676
+ isDryRun,
4677
+ listFilesRecursively,
3972
4678
  loadJson,
3973
4679
  loadSchema,
3974
4680
  logger,
4681
+ optionalEnv,
3975
4682
  parseCellReference,
3976
4683
  postFormDataApiCall,
3977
4684
  postJSONApiCall,
3978
4685
  postUrlEncodedApiCall,
3979
4686
  printMemoryFootprint,
3980
4687
  processFileStreams,
4688
+ processFromAsyncIterable,
3981
4689
  removeParasiteProperties,
4690
+ requireEnv,
3982
4691
  rowGeneratorFromCsv,
4692
+ runWithConcurrency,
3983
4693
  safePath,
3984
4694
  unzip,
3985
4695
  validateExcelConfig,