@whaly/connector-sdk 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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);
@@ -1087,6 +1274,7 @@ var Stream = class {
1087
1274
  // Private sync methods:
1088
1275
  async _syncRecords(parent) {
1089
1276
  let rowsSent = 0;
1277
+ const dryRunLimit = isDryRun() ? getDryRunLimit() : void 0;
1090
1278
  let recordSyncedMetrics = [];
1091
1279
  if (this.rowsSyncedMetricsConf.isEnabled() === true) {
1092
1280
  recordSyncedMetrics = getCounterMetrics(
@@ -1128,6 +1316,10 @@ var Stream = class {
1128
1316
  metric.increment();
1129
1317
  });
1130
1318
  rowsSent += 1;
1319
+ if (dryRunLimit !== void 0 && rowsSent >= dryRunLimit) {
1320
+ logger.info(`\u{1F6D1} Stream: ${this.streamId} - DRY_RUN_LIMIT reached (${dryRunLimit} records), stopping.`);
1321
+ break;
1322
+ }
1131
1323
  if (rowsSent % 1e3 === 0) {
1132
1324
  const percentStr = this.totalRows ? ` (${Math.round(rowsSent / this.totalRows * 100)}%)` : "";
1133
1325
  logger.info(`\u23F3 Stream: ${this.streamId} - ${rowsSent} records synced so far...${percentStr}`);
@@ -1819,15 +2011,15 @@ var import_fs_extra = __toESM(require("fs-extra"));
1819
2011
  var import_uniqueId = __toESM(require("lodash/uniqueId.js"));
1820
2012
  var createTemporaryFileStream = (streamId) => {
1821
2013
  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" });
2014
+ const path7 = `./tmp/${safePath(streamId)}-${(0, import_uniqueId.default)()}.tmp`;
2015
+ const writeStream = import_fs_extra.default.createWriteStream(path7, { flags: "w" });
1824
2016
  return {
1825
2017
  stream: writeStream,
1826
- path: path2
2018
+ path: path7
1827
2019
  };
1828
2020
  };
1829
- function safePath(path2) {
1830
- let formattedPath = path2;
2021
+ function safePath(path7) {
2022
+ let formattedPath = path7;
1831
2023
  formattedPath = formattedPath.replace(/^The\s+/gi, "");
1832
2024
  formattedPath = formattedPath.replace(/\s+(?:\(|\[)?Dis(?:c|k) (?:One|Two|Three|Four|Five|Six|Seven|Eight|Nine|Ten|\d)+(?:\)|\])?/gi, "");
1833
2025
  formattedPath = formattedPath.replace(/\s+/gi, "_");
@@ -1953,6 +2145,8 @@ var StreamState = class {
1953
2145
  // src/sdk/models/target/target.ts
1954
2146
  var import_async_mutex = require("async-mutex");
1955
2147
  var import_bluebird3 = __toESM(require("bluebird"));
2148
+ var import_fs_extra2 = __toESM(require("fs-extra"));
2149
+ var import_node_path2 = __toESM(require("path"));
1956
2150
  var semaphore = new import_async_mutex.Semaphore(1);
1957
2151
  var ITarget = class _ITarget {
1958
2152
  config;
@@ -2039,7 +2233,7 @@ var ITarget = class _ITarget {
2039
2233
  if (!stream) {
2040
2234
  throw new MissingSchemaError(`A record for stream:\`${streamId}\` was encountered before a corresponding schema.`, streamId);
2041
2235
  }
2042
- if (stream.getBatchedRowCount() % 1e4 === 0) {
2236
+ if (stream.getBatchedRowCount() % 1e4 === 0 && !isDryRun()) {
2043
2237
  await semaphore.runExclusive(async () => {
2044
2238
  const shouldUploadIntermediateBatch = this.shouldUploadIntermediateBatch(streamId);
2045
2239
  if (shouldUploadIntermediateBatch) {
@@ -2125,7 +2319,9 @@ var ITarget = class _ITarget {
2125
2319
  const { hasBreakingChanges, changes } = this.analyzeSchemaChanges(prevSchema, newFlattenedSchema, streamId);
2126
2320
  if (hasBreakingChanges) {
2127
2321
  logger.info(`\u{1F525} StreamId=${streamId} - Detected BREAKING schema changes: ${changes.join(", ")}. Loading batched records before applying new schema.`);
2128
- await _ITarget.uploadSingleStreamToWarehouse(prevStreamState);
2322
+ if (!isDryRun()) {
2323
+ await _ITarget.uploadSingleStreamToWarehouse(prevStreamState);
2324
+ }
2129
2325
  } else {
2130
2326
  logger.info(`\u2705 StreamId=${streamId} - Detected NON-BREAKING schema changes: ${changes.join(", ")}. Continuing without uploading batched records.`);
2131
2327
  }
@@ -2149,36 +2345,41 @@ var ITarget = class _ITarget {
2149
2345
  streamState.setSchema(schema);
2150
2346
  streamState.setCompiledValidateFn(this.ajv.compile({ type: "object", properties: schema }));
2151
2347
  const streamReplicationMethod = this.streams[streamId]?.getReplicationMethod();
2152
- if (message.keyProperties.length === 0 && streamReplicationMethod !== "APPEND" /* APPEND */) {
2348
+ if (message.keyProperties.length === 0 && streamReplicationMethod === "INCREMENTAL" /* INCREMENTAL */) {
2153
2349
  throw new Error(`StreamId: ${message.stream} - \`key_properties\` field is required and can't be empty in SCHEMA message.
2154
2350
  Received SCHEMA message: ${JSON.stringify(message)}`);
2155
2351
  }
2156
2352
  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
2353
+ dbSync.renamedColumnStore.computeColumnNameForStream(streamId, schema);
2354
+ if (!isDryRun()) {
2355
+ await dbSync.updateSchemaInWarehouse(schema, message.keyProperties);
2356
+ const {
2357
+ database: databaseName,
2358
+ schema: schemaName
2359
+ } = this.config;
2360
+ const tableName = this.genTableName(streamId);
2361
+ const formattedRelationshipMessage = {
2362
+ ...message,
2363
+ keyProperties: message.keyProperties.map((k) => {
2364
+ const translation = streamState.getDbSync().renamedColumnStore.getColumnTranslation(message.stream, k);
2365
+ if (translation) {
2366
+ return translation;
2367
+ }
2368
+ return k;
2369
+ })
2179
2370
  };
2180
- await hook.writeSchema(input);
2181
- }, { concurrency: 3 });
2371
+ await import_bluebird3.default.map(this.schemaHooks, async (hook) => {
2372
+ const input = {
2373
+ databaseName,
2374
+ schemaName,
2375
+ tableName,
2376
+ message: formattedRelationshipMessage
2377
+ };
2378
+ await hook.writeSchema(input);
2379
+ }, { concurrency: 3 });
2380
+ } else {
2381
+ logger.info(`[DRY_RUN] Stream: ${streamId} - Skipping warehouse schema update and schema hooks`);
2382
+ }
2182
2383
  return Promise.resolve();
2183
2384
  } catch (err) {
2184
2385
  logger.error(`Error when handling schema event.`, err);
@@ -2199,6 +2400,29 @@ var ITarget = class _ITarget {
2199
2400
  };
2200
2401
  complete = async () => {
2201
2402
  try {
2403
+ if (isDryRun()) {
2404
+ logger.info(`[DRY_RUN] Data Extraction is complete. Copying tmp files to out/ instead of loading to warehouse.`);
2405
+ const outDir = import_node_path2.default.resolve("out");
2406
+ await import_fs_extra2.default.ensureDir(outDir);
2407
+ for (const streamId of Object.keys(this.streams)) {
2408
+ const streamState = this.streams[streamId];
2409
+ if (!streamState || streamState.getBatchedRowCount() === 0) continue;
2410
+ const fileToLoad = streamState.getFileToLoad();
2411
+ await new Promise((resolve, reject) => {
2412
+ fileToLoad.stream.end(() => {
2413
+ fileToLoad.stream.close((err) => {
2414
+ if (err) reject(err);
2415
+ else resolve();
2416
+ });
2417
+ });
2418
+ });
2419
+ const destPath = import_node_path2.default.join(outDir, `${safePath(streamId)}.ndjson`);
2420
+ await import_fs_extra2.default.copy(fileToLoad.path, destPath);
2421
+ logger.info(`[DRY_RUN] Stream: ${streamId} - Copied ${streamState.getBatchedRowCount()} records to ${destPath}`);
2422
+ await import_fs_extra2.default.remove(fileToLoad.path);
2423
+ }
2424
+ return;
2425
+ }
2202
2426
  logger.info(`\u{1F44D} Data Extraction is complete. Will load remaining batched stream records.`);
2203
2427
  await this.loadAllStreamsInWarehouse({ isFinalLoad: true });
2204
2428
  return Promise.resolve();
@@ -2631,7 +2855,7 @@ var formatHexComparison = (expected, actual) => {
2631
2855
  Expected: "${expected}" (${expectedInfo.length} chars) [${expectedInfo.hex}]
2632
2856
  Actual: "${actual}" (${actualInfo.length} chars) [${actualInfo.hex}]`;
2633
2857
  };
2634
- async function* rowGeneratorFromCsv(path2, fileConfig) {
2858
+ async function* rowGeneratorFromCsv(path7, fileConfig) {
2635
2859
  const isGeneratorConfigArray = Array.isArray(fileConfig.fields);
2636
2860
  const configKeys = isGeneratorConfigArray ? [] : Object.keys(fileConfig.fields);
2637
2861
  const csvOptions = { separator: fileConfig.separator };
@@ -2643,10 +2867,10 @@ async function* rowGeneratorFromCsv(path2, fileConfig) {
2643
2867
  let finalInputStream;
2644
2868
  const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
2645
2869
  if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
2646
- initialReadStream = (0, import_fs2.createReadStream)(path2);
2870
+ initialReadStream = (0, import_fs2.createReadStream)(path7);
2647
2871
  finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
2648
2872
  } else {
2649
- initialReadStream = (0, import_fs2.createReadStream)(path2);
2873
+ initialReadStream = (0, import_fs2.createReadStream)(path7);
2650
2874
  finalInputStream = initialReadStream.pipe(createStripBomTransform());
2651
2875
  }
2652
2876
  finalInputStream.pipe(csvStream);
@@ -2688,7 +2912,7 @@ async function countCsvLines(filePath) {
2688
2912
  }).on("end", () => resolve(Math.max(0, newlines - 1))).on("error", reject);
2689
2913
  });
2690
2914
  }
2691
- var checkCsvHeaderRow = async (path2, fileConfig) => {
2915
+ var checkCsvHeaderRow = async (path7, fileConfig) => {
2692
2916
  return new Promise((resolve, reject) => {
2693
2917
  const colsWithParsingConfig = Object.keys(fileConfig.fields).map((key) => key.trim());
2694
2918
  console.debug(`${logPrefix} CSV Expected columns:`, colsWithParsingConfig.map((col) => {
@@ -2705,10 +2929,10 @@ var checkCsvHeaderRow = async (path2, fileConfig) => {
2705
2929
  let finalInputStream;
2706
2930
  const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
2707
2931
  if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
2708
- initialReadStream = (0, import_fs2.createReadStream)(path2);
2932
+ initialReadStream = (0, import_fs2.createReadStream)(path7);
2709
2933
  finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
2710
2934
  } else {
2711
- initialReadStream = (0, import_fs2.createReadStream)(path2);
2935
+ initialReadStream = (0, import_fs2.createReadStream)(path7);
2712
2936
  finalInputStream = initialReadStream.pipe(createStripBomTransform());
2713
2937
  }
2714
2938
  let isFirstLine = true;
@@ -2734,7 +2958,7 @@ var checkCsvHeaderRow = async (path2, fileConfig) => {
2734
2958
  i,
2735
2959
  col,
2736
2960
  headers[i] || "<undefined>",
2737
- path2
2961
+ path7
2738
2962
  ));
2739
2963
  }
2740
2964
  });
@@ -2751,7 +2975,7 @@ var checkCsvHeaderRow = async (path2, fileConfig) => {
2751
2975
  throw new Error((0, import_util3.format)(
2752
2976
  `CSV missing expected column '%s' in file: %s`,
2753
2977
  col,
2754
- path2
2978
+ path7
2755
2979
  ));
2756
2980
  }
2757
2981
  });
@@ -2777,7 +3001,7 @@ var writeDataToCsv = async (fileName, data) => {
2777
3001
  try {
2778
3002
  const firstRow = data[0];
2779
3003
  if (!firstRow) {
2780
- console.log(`${logPrefix2} No data to write to CSV file=%s`, fileName);
3004
+ logger.info(`${logPrefix2} No data to write to CSV file=%s`, fileName);
2781
3005
  return;
2782
3006
  }
2783
3007
  const csvWriter = (0, import_csv_writer.createObjectCsvWriter)({
@@ -2787,7 +3011,7 @@ var writeDataToCsv = async (fileName, data) => {
2787
3011
  })
2788
3012
  });
2789
3013
  await csvWriter.writeRecords(data);
2790
- console.log(`${logPrefix2} CSV file=%s written successfully`, fileName);
3014
+ logger.info(`${logPrefix2} CSV file=%s written successfully`, fileName);
2791
3015
  } catch (err) {
2792
3016
  if (err instanceof Error) {
2793
3017
  throw new Error((0, import_util4.format)(`error while writing csv file=%s, err: %s`, fileName, err.message));
@@ -2795,15 +3019,17 @@ var writeDataToCsv = async (fileName, data) => {
2795
3019
  throw err;
2796
3020
  }
2797
3021
  };
2798
- var writeGeneratorToCSV = async (generator, outputFileName) => {
3022
+ var writeGeneratorToCSV = async (generator, outputFileName, options) => {
3023
+ const batchSize = options?.batchSize ?? 1e4;
3024
+ const alwaysQuote = options?.alwaysQuote ?? true;
2799
3025
  try {
2800
3026
  let rowCount = 0;
2801
3027
  const firstDataRow = (await generator.next()).value;
2802
3028
  if (!firstDataRow) {
2803
- console.log(`${logPrefix2} [%s] first data row of csv empty, skipping`, outputFileName);
3029
+ logger.info(`${logPrefix2} [%s] first data row of csv empty, skipping`, outputFileName);
2804
3030
  return 0;
2805
3031
  }
2806
- console.log(`${logPrefix2} [%s] inferring headers from first data row`, outputFileName);
3032
+ logger.info(`${logPrefix2} [%s] inferring headers from first data row`, outputFileName);
2807
3033
  const headers = Object.keys(firstDataRow).map((col) => {
2808
3034
  return {
2809
3035
  id: col,
@@ -2813,14 +3039,14 @@ var writeGeneratorToCSV = async (generator, outputFileName) => {
2813
3039
  const csvWriter = (0, import_csv_writer.createObjectCsvWriter)({
2814
3040
  path: outputFileName,
2815
3041
  header: headers,
2816
- alwaysQuote: true
3042
+ alwaysQuote
2817
3043
  });
2818
- console.log(`${logPrefix2} [%s] writing csv`, outputFileName);
3044
+ logger.info(`${logPrefix2} [%s] writing csv`, outputFileName);
2819
3045
  let batch = [firstDataRow];
2820
3046
  for await (const data of generator) {
2821
3047
  batch.push(data);
2822
3048
  rowCount++;
2823
- if (batch.length > 1e4) {
3049
+ if (batch.length >= batchSize) {
2824
3050
  try {
2825
3051
  await csvWriter.writeRecords(batch);
2826
3052
  } catch (err) {
@@ -2838,8 +3064,10 @@ var writeGeneratorToCSV = async (generator, outputFileName) => {
2838
3064
  batch = [];
2839
3065
  }
2840
3066
  }
2841
- await csvWriter.writeRecords(batch);
2842
- console.log(`${logPrefix2} [%s] csv written successfully with %d rows`, outputFileName, rowCount);
3067
+ if (batch.length > 0) {
3068
+ await csvWriter.writeRecords(batch);
3069
+ }
3070
+ logger.info(`${logPrefix2} [%s] csv written successfully with %d rows`, outputFileName, rowCount);
2843
3071
  return rowCount;
2844
3072
  } catch (err) {
2845
3073
  if (err instanceof Error) {
@@ -3125,9 +3353,65 @@ var FilePatterns = class {
3125
3353
  };
3126
3354
 
3127
3355
  // src/services/sftp.ts
3356
+ var import_node_path3 = __toESM(require("path"));
3357
+ var import_fs_extra3 = __toESM(require("fs-extra"));
3128
3358
  var import_ssh2_sftp_client = __toESM(require("ssh2-sftp-client"));
3129
3359
  var import_ssh2_sftp_client2 = __toESM(require("ssh2-sftp-client"));
3130
3360
  var SftpService = new import_ssh2_sftp_client.default();
3361
+ var logPrefix3 = "[sftp]";
3362
+ async function listFilesRecursively(client, remotePath, options) {
3363
+ const recursive = options?.recursive ?? true;
3364
+ const extensions = options?.extensions?.map((e) => e.toLowerCase());
3365
+ const maxIterations = options?.maxIterations ?? 1e3;
3366
+ const results = [];
3367
+ const dirs = [remotePath];
3368
+ let iterations = 0;
3369
+ while (dirs.length > 0) {
3370
+ if (iterations++ >= maxIterations) {
3371
+ logger.warn(`${logPrefix3} listFilesRecursively reached maxIterations (${maxIterations}), stopping.`);
3372
+ break;
3373
+ }
3374
+ const currentDir = dirs.shift();
3375
+ let entries;
3376
+ try {
3377
+ entries = await client.list(currentDir);
3378
+ } catch (err) {
3379
+ const msg = err instanceof Error ? err.message : String(err);
3380
+ logger.warn(`${logPrefix3} Failed to list ${currentDir}: ${msg}`);
3381
+ continue;
3382
+ }
3383
+ for (const entry of entries) {
3384
+ if (entry.type === "d") {
3385
+ if (recursive && entry.name !== "." && entry.name !== "..") {
3386
+ dirs.push(import_node_path3.default.posix.join(currentDir, entry.name));
3387
+ }
3388
+ continue;
3389
+ }
3390
+ if (entry.type !== "-") continue;
3391
+ if (extensions) {
3392
+ const ext = import_node_path3.default.extname(entry.name).toLowerCase();
3393
+ if (!extensions.includes(ext)) continue;
3394
+ }
3395
+ const fullPath = import_node_path3.default.posix.join(currentDir, entry.name);
3396
+ results.push({ ...entry, name: fullPath });
3397
+ }
3398
+ }
3399
+ logger.info(`${logPrefix3} listFilesRecursively found ${results.length} files under ${remotePath}`);
3400
+ return results;
3401
+ }
3402
+ async function downloadFiles(client, files, localDir, options) {
3403
+ const concurrency = options?.concurrency ?? 5;
3404
+ await import_fs_extra3.default.ensureDir(localDir);
3405
+ const tasks = files.map((file, index) => () => {
3406
+ const localName = `${index}_${import_node_path3.default.basename(file.name)}`;
3407
+ const localPath = import_node_path3.default.join(localDir, localName);
3408
+ logger.debug(`${logPrefix3} Downloading ${file.name} \u2192 ${localPath}`);
3409
+ return client.fastGet(file.name, localPath).then(() => localPath);
3410
+ });
3411
+ const results = await runWithConcurrency(tasks, concurrency);
3412
+ logger.info(`${logPrefix3} Downloaded ${results.length} files to ${localDir}`);
3413
+ return results;
3414
+ }
3131
3415
 
3132
3416
  // src/services/cloud-storage.ts
3133
3417
  var import_storage = require("@google-cloud/storage");
@@ -3135,7 +3419,7 @@ var import_util5 = require("util");
3135
3419
  var pathModule = __toESM(require("path"));
3136
3420
  var import_fs3 = require("fs");
3137
3421
  var import_crypto = require("crypto");
3138
- var logPrefix3 = "[CloudStorageService]";
3422
+ var logPrefix4 = "[CloudStorageService]";
3139
3423
  var tmpDir = "tmp";
3140
3424
  var CloudStorageService = class {
3141
3425
  storage;
@@ -3143,10 +3427,10 @@ var CloudStorageService = class {
3143
3427
  processedSuffix;
3144
3428
  supportedExtensions;
3145
3429
  path;
3146
- constructor(bucketName, path2, opts) {
3430
+ constructor(bucketName, path7, opts) {
3147
3431
  this.storage = new import_storage.Storage({ retryOptions: { autoRetry: true, maxRetries: 20 } });
3148
3432
  this.bucket = this.storage.bucket(bucketName);
3149
- this.path = path2;
3433
+ this.path = path7;
3150
3434
  this.processedSuffix = opts?.processedSuffix ?? ".processed";
3151
3435
  this.supportedExtensions = opts?.supportedExtensions ?? [];
3152
3436
  }
@@ -3187,7 +3471,7 @@ var CloudStorageService = class {
3187
3471
  try {
3188
3472
  const file = this.bucket.file(markerFileName);
3189
3473
  await file.save((0, import_util5.format)("Marked file %s as processed", fileName));
3190
- logger.info(`${logPrefix3} Marker file ${markerFileName} created successfully.`);
3474
+ logger.info(`${logPrefix4} Marker file ${markerFileName} created successfully.`);
3191
3475
  } catch (err) {
3192
3476
  if (err instanceof Error) {
3193
3477
  throw new Error((0, import_util5.format)(`error while writing marker file=%s, err:%s`, markerFileName, err.message));
@@ -3211,7 +3495,7 @@ var CloudStorageService = class {
3211
3495
  (0, import_fs3.unlinkSync)(destFilename);
3212
3496
  }
3213
3497
  await file.download({ destination: destFilename });
3214
- logger.info(`${logPrefix3} Downloaded ${fileName} to ${destFilename}`);
3498
+ logger.info(`${logPrefix4} Downloaded ${fileName} to ${destFilename}`);
3215
3499
  return destFilename;
3216
3500
  } catch (err) {
3217
3501
  if (err instanceof Error) {
@@ -3229,9 +3513,9 @@ var CloudStorageService = class {
3229
3513
  */
3230
3514
  async uploadFile(localPath, destPath) {
3231
3515
  try {
3232
- logger.info(`${logPrefix3} preparing to upload '%s' to '%s'`, localPath, destPath);
3516
+ logger.info(`${logPrefix4} preparing to upload '%s' to '%s'`, localPath, destPath);
3233
3517
  const [file] = await this.bucket.upload(localPath, { destination: destPath });
3234
- logger.info(`${logPrefix3} file %s has been successfully uploaded into %s`, localPath, destPath);
3518
+ logger.info(`${logPrefix4} file %s has been successfully uploaded into %s`, localPath, destPath);
3235
3519
  return file;
3236
3520
  } catch (err) {
3237
3521
  if (err instanceof Error) {
@@ -3307,6 +3591,376 @@ var unzip = async (zipFilePath, extractedPath) => {
3307
3591
  return extractedPath;
3308
3592
  };
3309
3593
 
3594
+ // src/services/cdn.ts
3595
+ var import_axios3 = __toESM(require("axios"));
3596
+ var import_axios_retry2 = __toESM(require("axios-retry"));
3597
+ var logPrefix5 = "[CdnService]";
3598
+ var MAX_RETRIES = 10;
3599
+ var CdnService = class {
3600
+ axiosClient;
3601
+ constructor(config) {
3602
+ const resolvedKey = getServiceAccountKey(config.serviceAccountKey);
3603
+ const resolvedEndpoint = getApiEndpoint(config.apiEndpoint);
3604
+ this.axiosClient = import_axios3.default.create({
3605
+ baseURL: resolvedEndpoint,
3606
+ timeout: 12e4,
3607
+ headers: {
3608
+ Authorization: `Bearer ${resolvedKey}`,
3609
+ Accept: "application/json"
3610
+ }
3611
+ });
3612
+ (0, import_axios_retry2.default)(this.axiosClient, {
3613
+ retries: MAX_RETRIES,
3614
+ retryCondition: (error) => {
3615
+ const status = error.response?.status ?? 0;
3616
+ return !error.response || status === 429 || status >= 500 && status < 600;
3617
+ },
3618
+ retryDelay: (retryCount, error) => {
3619
+ const retryAfter = error.response?.headers?.["retry-after"];
3620
+ if (retryAfter) {
3621
+ const seconds = Number(retryAfter);
3622
+ if (!isNaN(seconds) && seconds > 0) {
3623
+ logger.info(`${logPrefix5} Rate limited \u2014 waiting ${seconds}s (Retry-After header), attempt ${retryCount}/${MAX_RETRIES}`);
3624
+ return seconds * 1e3;
3625
+ }
3626
+ }
3627
+ const delay = import_axios_retry2.default.exponentialDelay(retryCount);
3628
+ logger.info(`${logPrefix5} Retrying in ${delay}ms (attempt ${retryCount}/${MAX_RETRIES}) for ${error.config?.method?.toUpperCase()} ${error.config?.url}`);
3629
+ return delay;
3630
+ }
3631
+ });
3632
+ }
3633
+ /**
3634
+ * Returns metadata for a CDN file via a HEAD request.
3635
+ * If the file does not exist, returns `{ exists: false, lastModified: null, contentType: null }`.
3636
+ * Falls back to not-existing if HEAD is not supported (405).
3637
+ */
3638
+ async getFileMetadata(cdnId, fileName) {
3639
+ const url = `/v1/cdn/${cdnId}/files/${encodeURIComponent(fileName)}`;
3640
+ try {
3641
+ const response = await this.axiosClient.head(url);
3642
+ const lastModifiedHeader = response.headers["last-modified"];
3643
+ const contentTypeHeader = response.headers["content-type"];
3644
+ return {
3645
+ exists: true,
3646
+ lastModified: lastModifiedHeader ? new Date(lastModifiedHeader) : null,
3647
+ contentType: contentTypeHeader ?? null
3648
+ };
3649
+ } catch (err) {
3650
+ if (import_axios3.default.isAxiosError(err)) {
3651
+ const status = err.response?.status;
3652
+ if (status === 404) return { exists: false, lastModified: null, contentType: null };
3653
+ if (status === 405) {
3654
+ logger.warn(`${logPrefix5} HEAD not supported for ${url}, treating file as absent`);
3655
+ return { exists: false, lastModified: null, contentType: null };
3656
+ }
3657
+ this.throwMeaningfulError(err);
3658
+ }
3659
+ throw err;
3660
+ }
3661
+ }
3662
+ /**
3663
+ * Checks whether a file already exists in the CDN via a HEAD request.
3664
+ * Returns true if the server responds 2xx, false on 404.
3665
+ * Falls back to false (treat as not existing) if HEAD is not supported (405).
3666
+ * No body is transferred.
3667
+ */
3668
+ async fileExists(cdnId, fileName) {
3669
+ return (await this.getFileMetadata(cdnId, fileName)).exists;
3670
+ }
3671
+ /**
3672
+ * Uploads a file buffer to the Whaly CDN.
3673
+ * The PUT is idempotent — uploading the same fileName overwrites the existing file.
3674
+ */
3675
+ async uploadFile(cdnId, fileName, fileBuffer) {
3676
+ const url = `/v1/cdn/${cdnId}/files/${encodeURIComponent(fileName)}`;
3677
+ try {
3678
+ logger.info(`${logPrefix5} Uploading ${fileName} (${fileBuffer.length} bytes) to CDN ${cdnId}`);
3679
+ const form = new FormData();
3680
+ form.append("file", new Blob([fileBuffer]), fileName);
3681
+ const response = await this.axiosClient.put(url, form);
3682
+ logger.info(`${logPrefix5} Successfully uploaded ${fileName} (status ${response.status})`);
3683
+ return {
3684
+ filePath: response.data?.filePath ?? `/org/${cdnId}/file/${fileName}`
3685
+ };
3686
+ } catch (err) {
3687
+ if (import_axios3.default.isAxiosError(err)) {
3688
+ this.throwMeaningfulError(err);
3689
+ }
3690
+ throw err;
3691
+ }
3692
+ }
3693
+ throwMeaningfulError(err) {
3694
+ const status = err.response?.status;
3695
+ if (status === 401 || status === 403) {
3696
+ throw new Error("Unauthorized - invalid or expired service account key");
3697
+ } else if (status === 404) {
3698
+ throw new Error("Organization or CDN not found");
3699
+ } else if (status === 500) {
3700
+ throw new Error("API server error - check CDN ID and service account permissions");
3701
+ } else {
3702
+ throw new Error(`API error: ${err.response?.statusText ?? err.message}`);
3703
+ }
3704
+ }
3705
+ };
3706
+
3707
+ // src/sdk/models/asset-tap/asset-stream.ts
3708
+ var AssetStream = class {
3709
+ streamId;
3710
+ config;
3711
+ replicationMode;
3712
+ constructor(streamId, config, replicationMode = "INCREMENTAL") {
3713
+ this.streamId = streamId;
3714
+ this.config = config;
3715
+ this.replicationMode = replicationMode;
3716
+ }
3717
+ async transformFile(downloadedPath, _entry) {
3718
+ return downloadedPath;
3719
+ }
3720
+ };
3721
+
3722
+ // src/sdk/models/asset-tap/asset-tap.ts
3723
+ var import_fs_extra4 = __toESM(require("fs-extra"));
3724
+ var import_node_path4 = __toESM(require("path"));
3725
+ var logPrefix6 = "[AssetTap]";
3726
+ function inferContentType(filePath, fallback) {
3727
+ const mime = getMimeType(filePath);
3728
+ return mime !== "application/octet-stream" ? mime : fallback;
3729
+ }
3730
+ function deriveManifestMode(streams) {
3731
+ const modes = new Set(streams.map((s) => s.replicationMode));
3732
+ if (modes.size > 1) {
3733
+ logger.warn(`${logPrefix6} Streams have mixed replication modes (${[...modes].join(", ")}). Manifest will report "FULL".`);
3734
+ return "FULL";
3735
+ }
3736
+ return streams[0]?.replicationMode ?? "INCREMENTAL";
3737
+ }
3738
+ var AssetTap = class {
3739
+ config;
3740
+ outputDir;
3741
+ streams = [];
3742
+ concurrency;
3743
+ target;
3744
+ constructor(target, config, outputDir = "out", concurrency = 5) {
3745
+ this.target = target;
3746
+ this.config = config;
3747
+ this.outputDir = outputDir;
3748
+ this.concurrency = concurrency;
3749
+ }
3750
+ async sync() {
3751
+ await this.init();
3752
+ const dryRun = isDryRun();
3753
+ const dryRunLimit = dryRun ? getDryRunLimit() : void 0;
3754
+ if (dryRun) {
3755
+ logger.info(`${logPrefix6} [DRY_RUN] mode active \u2014 skipping CDN checks and uploads`);
3756
+ await import_fs_extra4.default.emptyDir(this.outputDir);
3757
+ if (dryRunLimit !== void 0) {
3758
+ logger.info(`${logPrefix6} [DRY_RUN] Limit: ${dryRunLimit} assets per stream`);
3759
+ }
3760
+ } else if (process.env["DRY_RUN_LIMIT"] !== void 0) {
3761
+ logger.warn(`${logPrefix6} DRY_RUN_LIMIT is set but DRY_RUN is not active \u2014 limit will be ignored`);
3762
+ }
3763
+ const tmpDir2 = import_node_path4.default.join(this.outputDir, "tmp");
3764
+ await import_fs_extra4.default.ensureDir(tmpDir2);
3765
+ const streamManifests = [];
3766
+ let entryIndex = 0;
3767
+ let totalSummary = { total: 0, uploaded: 0, skipped: 0, errors: 0 };
3768
+ for (const stream of this.streams) {
3769
+ logger.info(`${logPrefix6} Processing stream: ${stream.streamId} (mode=${stream.replicationMode}, concurrency=${this.concurrency})`);
3770
+ const assetEntries = [];
3771
+ let streamAssetCount = 0;
3772
+ await processFromAsyncIterable(
3773
+ stream.listAssets(),
3774
+ async (entry) => {
3775
+ if (dryRunLimit !== void 0 && streamAssetCount >= dryRunLimit) {
3776
+ return "stop";
3777
+ }
3778
+ logger.debug(`${logPrefix6} Processing entry: ${entry.sourcePath}`);
3779
+ if (stream.replicationMode === "INCREMENTAL" && !dryRun) {
3780
+ const shouldSync = await this.target.shouldSync(entry);
3781
+ if (!shouldSync) {
3782
+ logger.debug(`${logPrefix6} Skipping ${entry.sourcePath} (up-to-date)`);
3783
+ assetEntries.push({
3784
+ sourcePath: entry.sourcePath,
3785
+ destinationPath: entry.destinationPath,
3786
+ downloadedPath: "",
3787
+ transformedPath: "",
3788
+ size: 0,
3789
+ contentType: entry.contentType,
3790
+ status: "skipped",
3791
+ transformed: false
3792
+ });
3793
+ return "continue";
3794
+ }
3795
+ }
3796
+ const fileName = `${entryIndex}_${import_node_path4.default.basename(entry.sourcePath)}`;
3797
+ const downloadedPath = import_node_path4.default.join(tmpDir2, fileName);
3798
+ let uploadPath = downloadedPath;
3799
+ entryIndex++;
3800
+ streamAssetCount++;
3801
+ try {
3802
+ await stream.downloadEntry(entry, downloadedPath);
3803
+ uploadPath = await stream.transformFile(downloadedPath, entry);
3804
+ const wasTransformed = uploadPath !== downloadedPath;
3805
+ const stat = await import_fs_extra4.default.stat(uploadPath);
3806
+ const processed = {
3807
+ entry,
3808
+ downloadedPath,
3809
+ uploadPath,
3810
+ wasTransformed,
3811
+ size: stat.size,
3812
+ contentType: wasTransformed ? inferContentType(uploadPath, entry.contentType) : entry.contentType
3813
+ };
3814
+ if (!dryRun) {
3815
+ await this.target.uploadAsset(processed);
3816
+ } else {
3817
+ const inspectPath = import_node_path4.default.join(this.outputDir, stream.streamId, entry.destinationPath);
3818
+ await import_fs_extra4.default.ensureDir(import_node_path4.default.dirname(inspectPath));
3819
+ await import_fs_extra4.default.copy(uploadPath, inspectPath);
3820
+ }
3821
+ assetEntries.push({
3822
+ sourcePath: entry.sourcePath,
3823
+ destinationPath: entry.destinationPath,
3824
+ downloadedPath,
3825
+ transformedPath: wasTransformed ? uploadPath : "",
3826
+ size: processed.size,
3827
+ contentType: processed.contentType,
3828
+ status: "uploaded",
3829
+ transformed: wasTransformed
3830
+ });
3831
+ logger.info(`${logPrefix6} ${dryRun ? "[DRY_RUN] Processed" : "Uploaded"} ${entry.sourcePath} \u2192 ${entry.destinationPath}`);
3832
+ } catch (err) {
3833
+ const message = err instanceof Error ? err.message : String(err);
3834
+ logger.error(`${logPrefix6} Failed to process ${entry.sourcePath}: ${message}`);
3835
+ assetEntries.push({
3836
+ sourcePath: entry.sourcePath,
3837
+ destinationPath: entry.destinationPath,
3838
+ downloadedPath: "",
3839
+ transformedPath: "",
3840
+ size: 0,
3841
+ contentType: entry.contentType,
3842
+ status: "error",
3843
+ transformed: false,
3844
+ error: message
3845
+ });
3846
+ } finally {
3847
+ await import_fs_extra4.default.remove(downloadedPath).catch(() => void 0);
3848
+ if (uploadPath !== downloadedPath) {
3849
+ await import_fs_extra4.default.remove(uploadPath).catch(() => void 0);
3850
+ }
3851
+ }
3852
+ if (dryRunLimit !== void 0 && streamAssetCount >= dryRunLimit) {
3853
+ logger.info(`${logPrefix6} [DRY_RUN] Reached limit of ${dryRunLimit} for stream "${stream.streamId}", stopping.`);
3854
+ return "stop";
3855
+ }
3856
+ return "continue";
3857
+ },
3858
+ this.concurrency
3859
+ );
3860
+ const streamSummary = {
3861
+ total: assetEntries.length,
3862
+ uploaded: assetEntries.filter((a) => a.status === "uploaded").length,
3863
+ skipped: assetEntries.filter((a) => a.status === "skipped").length,
3864
+ errors: assetEntries.filter((a) => a.status === "error").length
3865
+ };
3866
+ totalSummary.total += streamSummary.total;
3867
+ totalSummary.uploaded += streamSummary.uploaded;
3868
+ totalSummary.skipped += streamSummary.skipped;
3869
+ totalSummary.errors += streamSummary.errors;
3870
+ streamManifests.push({
3871
+ streamId: stream.streamId,
3872
+ mode: stream.replicationMode,
3873
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
3874
+ assets: assetEntries,
3875
+ summary: streamSummary
3876
+ });
3877
+ }
3878
+ const manifest = {
3879
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
3880
+ mode: deriveManifestMode(this.streams),
3881
+ streams: streamManifests,
3882
+ summary: totalSummary
3883
+ };
3884
+ await import_fs_extra4.default.ensureDir(this.outputDir);
3885
+ await import_fs_extra4.default.writeJson(import_node_path4.default.join(this.outputDir, "manifest.json"), manifest, { spaces: 2 });
3886
+ if (!dryRun) {
3887
+ await this.target.complete();
3888
+ }
3889
+ logger.info(`${logPrefix6} Sync complete. Uploaded=${totalSummary.uploaded} Skipped=${totalSummary.skipped} Errors=${totalSummary.errors}`);
3890
+ return manifest;
3891
+ }
3892
+ };
3893
+
3894
+ // src/sdk/models/asset-tap/image-transform.ts
3895
+ var import_sharp = __toESM(require("sharp"));
3896
+ var import_node_path5 = __toESM(require("path"));
3897
+ var SHARP_GRAVITY = {
3898
+ center: "centre",
3899
+ top: "north",
3900
+ bottom: "south",
3901
+ left: "west",
3902
+ right: "east",
3903
+ "top left": "northwest",
3904
+ "top right": "northeast",
3905
+ "bottom left": "southwest",
3906
+ "bottom right": "southeast"
3907
+ };
3908
+ function parseBackground(bg) {
3909
+ if (bg === void 0 || bg === "none" || bg === "transparent") {
3910
+ return { r: 0, g: 0, b: 0, alpha: 0 };
3911
+ }
3912
+ return bg;
3913
+ }
3914
+ var ImageTransform = class {
3915
+ /**
3916
+ * Converts an image to WebP format using sharp (libvips).
3917
+ *
3918
+ * The output file is written to the same directory as the input,
3919
+ * with the extension replaced by `.webp`.
3920
+ *
3921
+ * @param inputPath Path to the source image file.
3922
+ * @param options Optional resize / padding parameters.
3923
+ * @returns Path to the produced `.webp` file.
3924
+ */
3925
+ static async toWebp(inputPath, options) {
3926
+ const dir = import_node_path5.default.dirname(inputPath);
3927
+ const basename = import_node_path5.default.basename(inputPath, import_node_path5.default.extname(inputPath));
3928
+ const outputPath = import_node_path5.default.join(dir, `${basename}.webp`);
3929
+ let pipeline = (0, import_sharp.default)(inputPath);
3930
+ const hasSize = options.width !== void 0 && options.height !== void 0;
3931
+ if (hasSize) {
3932
+ const position = options.gravity !== void 0 ? SHARP_GRAVITY[options.gravity] ?? options.gravity : void 0;
3933
+ if (options.extent === true) {
3934
+ pipeline = pipeline.resize(options.width, options.height, {
3935
+ fit: "contain",
3936
+ position,
3937
+ background: parseBackground(options.background)
3938
+ });
3939
+ } else {
3940
+ pipeline = pipeline.resize(options.width, options.height, {
3941
+ fit: "inside",
3942
+ position
3943
+ });
3944
+ }
3945
+ }
3946
+ pipeline = pipeline.webp(
3947
+ options.quality !== void 0 ? { quality: options.quality } : {}
3948
+ );
3949
+ await pipeline.toFile(outputPath);
3950
+ return outputPath;
3951
+ }
3952
+ };
3953
+
3954
+ // src/sdk/models/asset-target/asset-target.ts
3955
+ var AssetTarget = class {
3956
+ config;
3957
+ constructor(config) {
3958
+ this.config = config;
3959
+ }
3960
+ async complete() {
3961
+ }
3962
+ };
3963
+
3310
3964
  // src/targets/bigquery/helpers.ts
3311
3965
  var safeColumnName = (key) => {
3312
3966
  let returnString = key;
@@ -3343,7 +3997,9 @@ var import_decimal = __toESM(require("decimal.js"));
3343
3997
  var import_dayjs5 = __toESM(require("dayjs"));
3344
3998
  var validateDateRange = (record, schema) => {
3345
3999
  Object.keys(schema).forEach((key) => {
3346
- const { format: format8 } = schema[key];
4000
+ const field = schema[key];
4001
+ if (!field) return;
4002
+ const { format: format8 } = field;
3347
4003
  if (format8 === "date-time") {
3348
4004
  if (record[key]) {
3349
4005
  const formattedDate = (0, import_dayjs5.default)(record[key]).format(defaultDateTimeFormat);
@@ -3573,6 +4229,12 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
3573
4229
  });
3574
4230
  };
3575
4231
  getReplaceQueries = () => {
4232
+ if (this.primaryKeys.length === 0) {
4233
+ return [
4234
+ `TRUNCATE TABLE ${this.sqlTableId};`,
4235
+ ...this.getAppendQueries()
4236
+ ];
4237
+ }
3576
4238
  return [
3577
4239
  `TRUNCATE TABLE ${this.sqlTableId};`,
3578
4240
  ...this.getMergeQueries()
@@ -3860,6 +4522,46 @@ var BigQueryTarget = class extends ITarget {
3860
4522
  convertNumberIntoDecimal = convertNumberIntoDecimal;
3861
4523
  };
3862
4524
 
4525
+ // src/targets/cdn/main.ts
4526
+ var import_fs_extra5 = __toESM(require("fs-extra"));
4527
+ var logPrefix7 = "[CdnAssetTarget]";
4528
+ var CdnAssetTarget = class extends AssetTarget {
4529
+ cdnService;
4530
+ constructor(config) {
4531
+ super(config);
4532
+ this.cdnService = new CdnService(config);
4533
+ }
4534
+ async shouldSync(entry) {
4535
+ const metadata = await this.cdnService.getFileMetadata(getCdnId(this.config.cdnId), entry.destinationPath);
4536
+ if (!metadata.exists) {
4537
+ logger.debug(`${logPrefix7} ${entry.destinationPath} not in CDN \u2192 will sync`);
4538
+ return true;
4539
+ }
4540
+ if (entry.lastModified === void 0) {
4541
+ logger.debug(`${logPrefix7} ${entry.destinationPath} source has no lastModified \u2192 will sync`);
4542
+ return true;
4543
+ }
4544
+ if (metadata.lastModified === null) {
4545
+ logger.debug(`${logPrefix7} ${entry.destinationPath} CDN has no lastModified \u2192 will sync`);
4546
+ return true;
4547
+ }
4548
+ const sourceIsNewer = entry.lastModified > metadata.lastModified;
4549
+ if (!sourceIsNewer) {
4550
+ logger.debug(`${logPrefix7} Skipping ${entry.destinationPath} (CDN is up-to-date)`);
4551
+ }
4552
+ return sourceIsNewer;
4553
+ }
4554
+ async uploadAsset(asset) {
4555
+ const fileBuffer = await import_fs_extra5.default.readFile(asset.uploadPath);
4556
+ await this.cdnService.uploadFile(
4557
+ getCdnId(this.config.cdnId),
4558
+ asset.entry.destinationPath,
4559
+ fileBuffer
4560
+ );
4561
+ logger.info(`${logPrefix7} Uploaded ${asset.entry.destinationPath} (${fileBuffer.length} bytes)`);
4562
+ }
4563
+ };
4564
+
3863
4565
  // src/state-providers/gcs/main.ts
3864
4566
  var import_util6 = require("util");
3865
4567
  var GCSStateProvider = class {
@@ -3901,9 +4603,14 @@ var GCSStateProvider = class {
3901
4603
  0 && (module.exports = {
3902
4604
  ALL_STREAM_ID_KEYWORD,
3903
4605
  API_CALLS_METRIC_NAME,
4606
+ AssetStream,
4607
+ AssetTap,
4608
+ AssetTarget,
3904
4609
  Authenticator,
3905
4610
  BATCH_INTERVAL_MS,
3906
4611
  BigQueryTarget,
4612
+ CdnAssetTarget,
4613
+ CdnService,
3907
4614
  CloudStorageService,
3908
4615
  CounterMetric,
3909
4616
  CsvExtractionConfigBuilder,
@@ -3917,6 +4624,8 @@ var GCSStateProvider = class {
3917
4624
  FileTap,
3918
4625
  GCSStateProvider,
3919
4626
  ITarget,
4627
+ ImageTransform,
4628
+ MIME_TYPES,
3920
4629
  MissingFieldInSchemaError,
3921
4630
  MissingSchemaError,
3922
4631
  RESTStream,
@@ -3938,6 +4647,7 @@ var GCSStateProvider = class {
3938
4647
  _greaterThan,
3939
4648
  addWhalyFields,
3940
4649
  checkCsvHeaderRow,
4650
+ collectPaginated,
3941
4651
  countCsvLines,
3942
4652
  createCellReference,
3943
4653
  createCsvStreamConfig,
@@ -3945,6 +4655,7 @@ var GCSStateProvider = class {
3945
4655
  createExcelStreamConfig,
3946
4656
  createTemporaryFileStream,
3947
4657
  csvFieldsToJsonSchema,
4658
+ downloadFiles,
3948
4659
  excelColumnToIndex,
3949
4660
  excelFieldsToJsonSchema,
3950
4661
  excelRowToIndex,
@@ -3957,29 +4668,40 @@ var GCSStateProvider = class {
3957
4668
  findAllMatchingConfigs,
3958
4669
  flattenSchema,
3959
4670
  getAllMetrics,
4671
+ getApiEndpoint,
3960
4672
  getAxiosInstance,
4673
+ getCdnId,
3961
4674
  getCounterMetric,
3962
4675
  getCounterMetrics,
3963
4676
  getDownloadFileApiCall,
4677
+ getDryRunLimit,
3964
4678
  getJSONApiCall,
3965
4679
  getJSONApiCallWithFullResponse,
4680
+ getMimeType,
3966
4681
  getRawJSONApiCall,
4682
+ getServiceAccountKey,
3967
4683
  gracefulExit,
3968
4684
  haltAndCatchFire,
3969
4685
  incrementStreamState,
3970
4686
  indexToExcelColumn,
3971
4687
  indexToExcelRow,
4688
+ isDryRun,
4689
+ listFilesRecursively,
3972
4690
  loadJson,
3973
4691
  loadSchema,
3974
4692
  logger,
4693
+ optionalEnv,
3975
4694
  parseCellReference,
3976
4695
  postFormDataApiCall,
3977
4696
  postJSONApiCall,
3978
4697
  postUrlEncodedApiCall,
3979
4698
  printMemoryFootprint,
3980
4699
  processFileStreams,
4700
+ processFromAsyncIterable,
3981
4701
  removeParasiteProperties,
4702
+ requireEnv,
3982
4703
  rowGeneratorFromCsv,
4704
+ runWithConcurrency,
3983
4705
  safePath,
3984
4706
  unzip,
3985
4707
  validateExcelConfig,