@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.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,10 +2140,13 @@ 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;
1959
2148
  syncedAtColumnName;
2149
+ syncedAtColumnUseLegacyStringType;
1960
2150
  schemaHooks;
1961
2151
  syncTime;
1962
2152
  stateProvider;
@@ -1973,6 +2163,7 @@ var ITarget = class _ITarget {
1973
2163
  constructor(config, stateProvider) {
1974
2164
  this.config = config;
1975
2165
  this.syncedAtColumnName = config.syncedAtColumnName ?? DEFAULT_SYNCED_AT_COLUMN;
2166
+ this.syncedAtColumnUseLegacyStringType = config.syncedAtColumnUseLegacyStringType ?? false;
1976
2167
  this.stateProvider = stateProvider;
1977
2168
  this.schemaHooks = [];
1978
2169
  this.streams = {};
@@ -2037,7 +2228,7 @@ var ITarget = class _ITarget {
2037
2228
  if (!stream) {
2038
2229
  throw new MissingSchemaError(`A record for stream:\`${streamId}\` was encountered before a corresponding schema.`, streamId);
2039
2230
  }
2040
- if (stream.getBatchedRowCount() % 1e4 === 0) {
2231
+ if (stream.getBatchedRowCount() % 1e4 === 0 && !isDryRun()) {
2041
2232
  await semaphore.runExclusive(async () => {
2042
2233
  const shouldUploadIntermediateBatch = this.shouldUploadIntermediateBatch(streamId);
2043
2234
  if (shouldUploadIntermediateBatch) {
@@ -2123,7 +2314,9 @@ var ITarget = class _ITarget {
2123
2314
  const { hasBreakingChanges, changes } = this.analyzeSchemaChanges(prevSchema, newFlattenedSchema, streamId);
2124
2315
  if (hasBreakingChanges) {
2125
2316
  logger.info(`\u{1F525} StreamId=${streamId} - Detected BREAKING schema changes: ${changes.join(", ")}. Loading batched records before applying new schema.`);
2126
- await _ITarget.uploadSingleStreamToWarehouse(prevStreamState);
2317
+ if (!isDryRun()) {
2318
+ await _ITarget.uploadSingleStreamToWarehouse(prevStreamState);
2319
+ }
2127
2320
  } else {
2128
2321
  logger.info(`\u2705 StreamId=${streamId} - Detected NON-BREAKING schema changes: ${changes.join(", ")}. Continuing without uploading batched records.`);
2129
2322
  }
@@ -2135,10 +2328,7 @@ var ITarget = class _ITarget {
2135
2328
  throw new MissingSchemaError(`Stream: ${streamId} - Received SCHEMA message before stream state was initialized.`, streamId);
2136
2329
  }
2137
2330
  const schema = flattenSchema(streamId, message.schema);
2138
- schema[this.syncedAtColumnName] = {
2139
- format: "date-time",
2140
- type: ["null", "string"]
2141
- };
2331
+ schema[this.syncedAtColumnName] = this.syncedAtColumnUseLegacyStringType ? { type: ["null", "string"] } : { format: "date-time", type: ["null", "string"] };
2142
2332
  const dbSync = streamState.getDbSync();
2143
2333
  Object.keys(schema).forEach((fieldUnsafeKey) => {
2144
2334
  const fieldDef = schema[fieldUnsafeKey];
@@ -2155,31 +2345,35 @@ var ITarget = class _ITarget {
2155
2345
  Received SCHEMA message: ${JSON.stringify(message)}`);
2156
2346
  }
2157
2347
  streamState.setKeyProperties(message.keyProperties);
2158
- await dbSync.updateSchemaInWarehouse(schema, message.keyProperties);
2159
- const {
2160
- database: databaseName,
2161
- schema: schemaName
2162
- } = this.config;
2163
- const tableName = this.genTableName(streamId);
2164
- const formattedRelationshipMessage = {
2165
- ...message,
2166
- keyProperties: message.keyProperties.map((k) => {
2167
- const translation = streamState.getDbSync().renamedColumnStore.getColumnTranslation(message.stream, k);
2168
- if (translation) {
2169
- return translation;
2170
- }
2171
- return k;
2172
- })
2173
- };
2174
- await import_bluebird3.default.map(this.schemaHooks, async (hook) => {
2175
- const input = {
2176
- databaseName,
2177
- schemaName,
2178
- tableName,
2179
- 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
+ })
2180
2364
  };
2181
- await hook.writeSchema(input);
2182
- }, { 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
+ }
2183
2377
  return Promise.resolve();
2184
2378
  } catch (err) {
2185
2379
  logger.error(`Error when handling schema event.`, err);
@@ -2200,6 +2394,29 @@ var ITarget = class _ITarget {
2200
2394
  };
2201
2395
  complete = async () => {
2202
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
+ }
2203
2420
  logger.info(`\u{1F44D} Data Extraction is complete. Will load remaining batched stream records.`);
2204
2421
  await this.loadAllStreamsInWarehouse({ isFinalLoad: true });
2205
2422
  return Promise.resolve();
@@ -2632,7 +2849,7 @@ var formatHexComparison = (expected, actual) => {
2632
2849
  Expected: "${expected}" (${expectedInfo.length} chars) [${expectedInfo.hex}]
2633
2850
  Actual: "${actual}" (${actualInfo.length} chars) [${actualInfo.hex}]`;
2634
2851
  };
2635
- async function* rowGeneratorFromCsv(path2, fileConfig) {
2852
+ async function* rowGeneratorFromCsv(path7, fileConfig) {
2636
2853
  const isGeneratorConfigArray = Array.isArray(fileConfig.fields);
2637
2854
  const configKeys = isGeneratorConfigArray ? [] : Object.keys(fileConfig.fields);
2638
2855
  const csvOptions = { separator: fileConfig.separator };
@@ -2644,10 +2861,10 @@ async function* rowGeneratorFromCsv(path2, fileConfig) {
2644
2861
  let finalInputStream;
2645
2862
  const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
2646
2863
  if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
2647
- initialReadStream = (0, import_fs2.createReadStream)(path2);
2864
+ initialReadStream = (0, import_fs2.createReadStream)(path7);
2648
2865
  finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
2649
2866
  } else {
2650
- initialReadStream = (0, import_fs2.createReadStream)(path2);
2867
+ initialReadStream = (0, import_fs2.createReadStream)(path7);
2651
2868
  finalInputStream = initialReadStream.pipe(createStripBomTransform());
2652
2869
  }
2653
2870
  finalInputStream.pipe(csvStream);
@@ -2689,7 +2906,7 @@ async function countCsvLines(filePath) {
2689
2906
  }).on("end", () => resolve(Math.max(0, newlines - 1))).on("error", reject);
2690
2907
  });
2691
2908
  }
2692
- var checkCsvHeaderRow = async (path2, fileConfig) => {
2909
+ var checkCsvHeaderRow = async (path7, fileConfig) => {
2693
2910
  return new Promise((resolve, reject) => {
2694
2911
  const colsWithParsingConfig = Object.keys(fileConfig.fields).map((key) => key.trim());
2695
2912
  console.debug(`${logPrefix} CSV Expected columns:`, colsWithParsingConfig.map((col) => {
@@ -2706,10 +2923,10 @@ var checkCsvHeaderRow = async (path2, fileConfig) => {
2706
2923
  let finalInputStream;
2707
2924
  const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
2708
2925
  if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
2709
- initialReadStream = (0, import_fs2.createReadStream)(path2);
2926
+ initialReadStream = (0, import_fs2.createReadStream)(path7);
2710
2927
  finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
2711
2928
  } else {
2712
- initialReadStream = (0, import_fs2.createReadStream)(path2);
2929
+ initialReadStream = (0, import_fs2.createReadStream)(path7);
2713
2930
  finalInputStream = initialReadStream.pipe(createStripBomTransform());
2714
2931
  }
2715
2932
  let isFirstLine = true;
@@ -2735,7 +2952,7 @@ var checkCsvHeaderRow = async (path2, fileConfig) => {
2735
2952
  i,
2736
2953
  col,
2737
2954
  headers[i] || "<undefined>",
2738
- path2
2955
+ path7
2739
2956
  ));
2740
2957
  }
2741
2958
  });
@@ -2752,7 +2969,7 @@ var checkCsvHeaderRow = async (path2, fileConfig) => {
2752
2969
  throw new Error((0, import_util3.format)(
2753
2970
  `CSV missing expected column '%s' in file: %s`,
2754
2971
  col,
2755
- path2
2972
+ path7
2756
2973
  ));
2757
2974
  }
2758
2975
  });
@@ -2778,7 +2995,7 @@ var writeDataToCsv = async (fileName, data) => {
2778
2995
  try {
2779
2996
  const firstRow = data[0];
2780
2997
  if (!firstRow) {
2781
- 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);
2782
2999
  return;
2783
3000
  }
2784
3001
  const csvWriter = (0, import_csv_writer.createObjectCsvWriter)({
@@ -2788,7 +3005,7 @@ var writeDataToCsv = async (fileName, data) => {
2788
3005
  })
2789
3006
  });
2790
3007
  await csvWriter.writeRecords(data);
2791
- console.log(`${logPrefix2} CSV file=%s written successfully`, fileName);
3008
+ logger.info(`${logPrefix2} CSV file=%s written successfully`, fileName);
2792
3009
  } catch (err) {
2793
3010
  if (err instanceof Error) {
2794
3011
  throw new Error((0, import_util4.format)(`error while writing csv file=%s, err: %s`, fileName, err.message));
@@ -2796,15 +3013,17 @@ var writeDataToCsv = async (fileName, data) => {
2796
3013
  throw err;
2797
3014
  }
2798
3015
  };
2799
- var writeGeneratorToCSV = async (generator, outputFileName) => {
3016
+ var writeGeneratorToCSV = async (generator, outputFileName, options) => {
3017
+ const batchSize = options?.batchSize ?? 1e4;
3018
+ const alwaysQuote = options?.alwaysQuote ?? true;
2800
3019
  try {
2801
3020
  let rowCount = 0;
2802
3021
  const firstDataRow = (await generator.next()).value;
2803
3022
  if (!firstDataRow) {
2804
- 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);
2805
3024
  return 0;
2806
3025
  }
2807
- console.log(`${logPrefix2} [%s] inferring headers from first data row`, outputFileName);
3026
+ logger.info(`${logPrefix2} [%s] inferring headers from first data row`, outputFileName);
2808
3027
  const headers = Object.keys(firstDataRow).map((col) => {
2809
3028
  return {
2810
3029
  id: col,
@@ -2814,14 +3033,14 @@ var writeGeneratorToCSV = async (generator, outputFileName) => {
2814
3033
  const csvWriter = (0, import_csv_writer.createObjectCsvWriter)({
2815
3034
  path: outputFileName,
2816
3035
  header: headers,
2817
- alwaysQuote: true
3036
+ alwaysQuote
2818
3037
  });
2819
- console.log(`${logPrefix2} [%s] writing csv`, outputFileName);
3038
+ logger.info(`${logPrefix2} [%s] writing csv`, outputFileName);
2820
3039
  let batch = [firstDataRow];
2821
3040
  for await (const data of generator) {
2822
3041
  batch.push(data);
2823
3042
  rowCount++;
2824
- if (batch.length > 1e4) {
3043
+ if (batch.length >= batchSize) {
2825
3044
  try {
2826
3045
  await csvWriter.writeRecords(batch);
2827
3046
  } catch (err) {
@@ -2839,8 +3058,10 @@ var writeGeneratorToCSV = async (generator, outputFileName) => {
2839
3058
  batch = [];
2840
3059
  }
2841
3060
  }
2842
- await csvWriter.writeRecords(batch);
2843
- 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);
2844
3065
  return rowCount;
2845
3066
  } catch (err) {
2846
3067
  if (err instanceof Error) {
@@ -3126,9 +3347,65 @@ var FilePatterns = class {
3126
3347
  };
3127
3348
 
3128
3349
  // src/services/sftp.ts
3350
+ var import_node_path3 = __toESM(require("path"));
3351
+ var import_fs_extra3 = __toESM(require("fs-extra"));
3129
3352
  var import_ssh2_sftp_client = __toESM(require("ssh2-sftp-client"));
3130
3353
  var import_ssh2_sftp_client2 = __toESM(require("ssh2-sftp-client"));
3131
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
+ }
3132
3409
 
3133
3410
  // src/services/cloud-storage.ts
3134
3411
  var import_storage = require("@google-cloud/storage");
@@ -3136,7 +3413,7 @@ var import_util5 = require("util");
3136
3413
  var pathModule = __toESM(require("path"));
3137
3414
  var import_fs3 = require("fs");
3138
3415
  var import_crypto = require("crypto");
3139
- var logPrefix3 = "[CloudStorageService]";
3416
+ var logPrefix4 = "[CloudStorageService]";
3140
3417
  var tmpDir = "tmp";
3141
3418
  var CloudStorageService = class {
3142
3419
  storage;
@@ -3144,10 +3421,10 @@ var CloudStorageService = class {
3144
3421
  processedSuffix;
3145
3422
  supportedExtensions;
3146
3423
  path;
3147
- constructor(bucketName, path2, opts) {
3424
+ constructor(bucketName, path7, opts) {
3148
3425
  this.storage = new import_storage.Storage({ retryOptions: { autoRetry: true, maxRetries: 20 } });
3149
3426
  this.bucket = this.storage.bucket(bucketName);
3150
- this.path = path2;
3427
+ this.path = path7;
3151
3428
  this.processedSuffix = opts?.processedSuffix ?? ".processed";
3152
3429
  this.supportedExtensions = opts?.supportedExtensions ?? [];
3153
3430
  }
@@ -3188,7 +3465,7 @@ var CloudStorageService = class {
3188
3465
  try {
3189
3466
  const file = this.bucket.file(markerFileName);
3190
3467
  await file.save((0, import_util5.format)("Marked file %s as processed", fileName));
3191
- logger.info(`${logPrefix3} Marker file ${markerFileName} created successfully.`);
3468
+ logger.info(`${logPrefix4} Marker file ${markerFileName} created successfully.`);
3192
3469
  } catch (err) {
3193
3470
  if (err instanceof Error) {
3194
3471
  throw new Error((0, import_util5.format)(`error while writing marker file=%s, err:%s`, markerFileName, err.message));
@@ -3212,7 +3489,7 @@ var CloudStorageService = class {
3212
3489
  (0, import_fs3.unlinkSync)(destFilename);
3213
3490
  }
3214
3491
  await file.download({ destination: destFilename });
3215
- logger.info(`${logPrefix3} Downloaded ${fileName} to ${destFilename}`);
3492
+ logger.info(`${logPrefix4} Downloaded ${fileName} to ${destFilename}`);
3216
3493
  return destFilename;
3217
3494
  } catch (err) {
3218
3495
  if (err instanceof Error) {
@@ -3230,9 +3507,9 @@ var CloudStorageService = class {
3230
3507
  */
3231
3508
  async uploadFile(localPath, destPath) {
3232
3509
  try {
3233
- logger.info(`${logPrefix3} preparing to upload '%s' to '%s'`, localPath, destPath);
3510
+ logger.info(`${logPrefix4} preparing to upload '%s' to '%s'`, localPath, destPath);
3234
3511
  const [file] = await this.bucket.upload(localPath, { destination: destPath });
3235
- 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);
3236
3513
  return file;
3237
3514
  } catch (err) {
3238
3515
  if (err instanceof Error) {
@@ -3308,6 +3585,376 @@ var unzip = async (zipFilePath, extractedPath) => {
3308
3585
  return extractedPath;
3309
3586
  };
3310
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
+
3311
3958
  // src/targets/bigquery/helpers.ts
3312
3959
  var safeColumnName = (key) => {
3313
3960
  let returnString = key;
@@ -3344,7 +3991,9 @@ var import_decimal = __toESM(require("decimal.js"));
3344
3991
  var import_dayjs5 = __toESM(require("dayjs"));
3345
3992
  var validateDateRange = (record, schema) => {
3346
3993
  Object.keys(schema).forEach((key) => {
3347
- const { format: format8 } = schema[key];
3994
+ const field = schema[key];
3995
+ if (!field) return;
3996
+ const { format: format8 } = field;
3348
3997
  if (format8 === "date-time") {
3349
3998
  if (record[key]) {
3350
3999
  const formattedDate = (0, import_dayjs5.default)(record[key]).format(defaultDateTimeFormat);
@@ -3861,6 +4510,46 @@ var BigQueryTarget = class extends ITarget {
3861
4510
  convertNumberIntoDecimal = convertNumberIntoDecimal;
3862
4511
  };
3863
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
+
3864
4553
  // src/state-providers/gcs/main.ts
3865
4554
  var import_util6 = require("util");
3866
4555
  var GCSStateProvider = class {
@@ -3902,9 +4591,14 @@ var GCSStateProvider = class {
3902
4591
  0 && (module.exports = {
3903
4592
  ALL_STREAM_ID_KEYWORD,
3904
4593
  API_CALLS_METRIC_NAME,
4594
+ AssetStream,
4595
+ AssetTap,
4596
+ AssetTarget,
3905
4597
  Authenticator,
3906
4598
  BATCH_INTERVAL_MS,
3907
4599
  BigQueryTarget,
4600
+ CdnAssetTarget,
4601
+ CdnService,
3908
4602
  CloudStorageService,
3909
4603
  CounterMetric,
3910
4604
  CsvExtractionConfigBuilder,
@@ -3918,6 +4612,8 @@ var GCSStateProvider = class {
3918
4612
  FileTap,
3919
4613
  GCSStateProvider,
3920
4614
  ITarget,
4615
+ ImageTransform,
4616
+ MIME_TYPES,
3921
4617
  MissingFieldInSchemaError,
3922
4618
  MissingSchemaError,
3923
4619
  RESTStream,
@@ -3939,6 +4635,7 @@ var GCSStateProvider = class {
3939
4635
  _greaterThan,
3940
4636
  addWhalyFields,
3941
4637
  checkCsvHeaderRow,
4638
+ collectPaginated,
3942
4639
  countCsvLines,
3943
4640
  createCellReference,
3944
4641
  createCsvStreamConfig,
@@ -3946,6 +4643,7 @@ var GCSStateProvider = class {
3946
4643
  createExcelStreamConfig,
3947
4644
  createTemporaryFileStream,
3948
4645
  csvFieldsToJsonSchema,
4646
+ downloadFiles,
3949
4647
  excelColumnToIndex,
3950
4648
  excelFieldsToJsonSchema,
3951
4649
  excelRowToIndex,
@@ -3958,29 +4656,40 @@ var GCSStateProvider = class {
3958
4656
  findAllMatchingConfigs,
3959
4657
  flattenSchema,
3960
4658
  getAllMetrics,
4659
+ getApiEndpoint,
3961
4660
  getAxiosInstance,
4661
+ getCdnId,
3962
4662
  getCounterMetric,
3963
4663
  getCounterMetrics,
3964
4664
  getDownloadFileApiCall,
4665
+ getDryRunLimit,
3965
4666
  getJSONApiCall,
3966
4667
  getJSONApiCallWithFullResponse,
4668
+ getMimeType,
3967
4669
  getRawJSONApiCall,
4670
+ getServiceAccountKey,
3968
4671
  gracefulExit,
3969
4672
  haltAndCatchFire,
3970
4673
  incrementStreamState,
3971
4674
  indexToExcelColumn,
3972
4675
  indexToExcelRow,
4676
+ isDryRun,
4677
+ listFilesRecursively,
3973
4678
  loadJson,
3974
4679
  loadSchema,
3975
4680
  logger,
4681
+ optionalEnv,
3976
4682
  parseCellReference,
3977
4683
  postFormDataApiCall,
3978
4684
  postJSONApiCall,
3979
4685
  postUrlEncodedApiCall,
3980
4686
  printMemoryFootprint,
3981
4687
  processFileStreams,
4688
+ processFromAsyncIterable,
3982
4689
  removeParasiteProperties,
4690
+ requireEnv,
3983
4691
  rowGeneratorFromCsv,
4692
+ runWithConcurrency,
3984
4693
  safePath,
3985
4694
  unzip,
3986
4695
  validateExcelConfig,