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