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