@whaly/connector-sdk 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +297 -4
- package/dist/index.d.ts +297 -4
- package/dist/index.js +782 -60
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +764 -62
- 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);
|
|
@@ -966,6 +1133,7 @@ var Stream = class {
|
|
|
966
1133
|
// Private sync methods:
|
|
967
1134
|
async _syncRecords(parent) {
|
|
968
1135
|
let rowsSent = 0;
|
|
1136
|
+
const dryRunLimit = isDryRun() ? getDryRunLimit() : void 0;
|
|
969
1137
|
let recordSyncedMetrics = [];
|
|
970
1138
|
if (this.rowsSyncedMetricsConf.isEnabled() === true) {
|
|
971
1139
|
recordSyncedMetrics = getCounterMetrics(
|
|
@@ -1007,6 +1175,10 @@ var Stream = class {
|
|
|
1007
1175
|
metric.increment();
|
|
1008
1176
|
});
|
|
1009
1177
|
rowsSent += 1;
|
|
1178
|
+
if (dryRunLimit !== void 0 && rowsSent >= dryRunLimit) {
|
|
1179
|
+
logger.info(`\u{1F6D1} Stream: ${this.streamId} - DRY_RUN_LIMIT reached (${dryRunLimit} records), stopping.`);
|
|
1180
|
+
break;
|
|
1181
|
+
}
|
|
1010
1182
|
if (rowsSent % 1e3 === 0) {
|
|
1011
1183
|
const percentStr = this.totalRows ? ` (${Math.round(rowsSent / this.totalRows * 100)}%)` : "";
|
|
1012
1184
|
logger.info(`\u23F3 Stream: ${this.streamId} - ${rowsSent} records synced so far...${percentStr}`);
|
|
@@ -1698,15 +1870,15 @@ import fs2 from "fs-extra";
|
|
|
1698
1870
|
import uniqueId from "lodash/uniqueId.js";
|
|
1699
1871
|
var createTemporaryFileStream = (streamId) => {
|
|
1700
1872
|
fs2.ensureDirSync("./tmp");
|
|
1701
|
-
const
|
|
1702
|
-
const writeStream = fs2.createWriteStream(
|
|
1873
|
+
const path7 = `./tmp/${safePath(streamId)}-${uniqueId()}.tmp`;
|
|
1874
|
+
const writeStream = fs2.createWriteStream(path7, { flags: "w" });
|
|
1703
1875
|
return {
|
|
1704
1876
|
stream: writeStream,
|
|
1705
|
-
path:
|
|
1877
|
+
path: path7
|
|
1706
1878
|
};
|
|
1707
1879
|
};
|
|
1708
|
-
function safePath(
|
|
1709
|
-
let formattedPath =
|
|
1880
|
+
function safePath(path7) {
|
|
1881
|
+
let formattedPath = path7;
|
|
1710
1882
|
formattedPath = formattedPath.replace(/^The\s+/gi, "");
|
|
1711
1883
|
formattedPath = formattedPath.replace(/\s+(?:\(|\[)?Dis(?:c|k) (?:One|Two|Three|Four|Five|Six|Seven|Eight|Nine|Ten|\d)+(?:\)|\])?/gi, "");
|
|
1712
1884
|
formattedPath = formattedPath.replace(/\s+/gi, "_");
|
|
@@ -1832,6 +2004,8 @@ var StreamState = class {
|
|
|
1832
2004
|
// src/sdk/models/target/target.ts
|
|
1833
2005
|
import { Semaphore } from "async-mutex";
|
|
1834
2006
|
import Bluebird3 from "bluebird";
|
|
2007
|
+
import fs4 from "fs-extra";
|
|
2008
|
+
import path2 from "path";
|
|
1835
2009
|
var semaphore = new Semaphore(1);
|
|
1836
2010
|
var ITarget = class _ITarget {
|
|
1837
2011
|
config;
|
|
@@ -1918,7 +2092,7 @@ var ITarget = class _ITarget {
|
|
|
1918
2092
|
if (!stream) {
|
|
1919
2093
|
throw new MissingSchemaError(`A record for stream:\`${streamId}\` was encountered before a corresponding schema.`, streamId);
|
|
1920
2094
|
}
|
|
1921
|
-
if (stream.getBatchedRowCount() % 1e4 === 0) {
|
|
2095
|
+
if (stream.getBatchedRowCount() % 1e4 === 0 && !isDryRun()) {
|
|
1922
2096
|
await semaphore.runExclusive(async () => {
|
|
1923
2097
|
const shouldUploadIntermediateBatch = this.shouldUploadIntermediateBatch(streamId);
|
|
1924
2098
|
if (shouldUploadIntermediateBatch) {
|
|
@@ -2004,7 +2178,9 @@ var ITarget = class _ITarget {
|
|
|
2004
2178
|
const { hasBreakingChanges, changes } = this.analyzeSchemaChanges(prevSchema, newFlattenedSchema, streamId);
|
|
2005
2179
|
if (hasBreakingChanges) {
|
|
2006
2180
|
logger.info(`\u{1F525} StreamId=${streamId} - Detected BREAKING schema changes: ${changes.join(", ")}. Loading batched records before applying new schema.`);
|
|
2007
|
-
|
|
2181
|
+
if (!isDryRun()) {
|
|
2182
|
+
await _ITarget.uploadSingleStreamToWarehouse(prevStreamState);
|
|
2183
|
+
}
|
|
2008
2184
|
} else {
|
|
2009
2185
|
logger.info(`\u2705 StreamId=${streamId} - Detected NON-BREAKING schema changes: ${changes.join(", ")}. Continuing without uploading batched records.`);
|
|
2010
2186
|
}
|
|
@@ -2028,36 +2204,41 @@ var ITarget = class _ITarget {
|
|
|
2028
2204
|
streamState.setSchema(schema);
|
|
2029
2205
|
streamState.setCompiledValidateFn(this.ajv.compile({ type: "object", properties: schema }));
|
|
2030
2206
|
const streamReplicationMethod = this.streams[streamId]?.getReplicationMethod();
|
|
2031
|
-
if (message.keyProperties.length === 0 && streamReplicationMethod
|
|
2207
|
+
if (message.keyProperties.length === 0 && streamReplicationMethod === "INCREMENTAL" /* INCREMENTAL */) {
|
|
2032
2208
|
throw new Error(`StreamId: ${message.stream} - \`key_properties\` field is required and can't be empty in SCHEMA message.
|
|
2033
2209
|
Received SCHEMA message: ${JSON.stringify(message)}`);
|
|
2034
2210
|
}
|
|
2035
2211
|
streamState.setKeyProperties(message.keyProperties);
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
const input = {
|
|
2054
|
-
databaseName,
|
|
2055
|
-
schemaName,
|
|
2056
|
-
tableName,
|
|
2057
|
-
message: formattedRelationshipMessage
|
|
2212
|
+
dbSync.renamedColumnStore.computeColumnNameForStream(streamId, schema);
|
|
2213
|
+
if (!isDryRun()) {
|
|
2214
|
+
await dbSync.updateSchemaInWarehouse(schema, message.keyProperties);
|
|
2215
|
+
const {
|
|
2216
|
+
database: databaseName,
|
|
2217
|
+
schema: schemaName
|
|
2218
|
+
} = this.config;
|
|
2219
|
+
const tableName = this.genTableName(streamId);
|
|
2220
|
+
const formattedRelationshipMessage = {
|
|
2221
|
+
...message,
|
|
2222
|
+
keyProperties: message.keyProperties.map((k) => {
|
|
2223
|
+
const translation = streamState.getDbSync().renamedColumnStore.getColumnTranslation(message.stream, k);
|
|
2224
|
+
if (translation) {
|
|
2225
|
+
return translation;
|
|
2226
|
+
}
|
|
2227
|
+
return k;
|
|
2228
|
+
})
|
|
2058
2229
|
};
|
|
2059
|
-
await
|
|
2060
|
-
|
|
2230
|
+
await Bluebird3.map(this.schemaHooks, async (hook) => {
|
|
2231
|
+
const input = {
|
|
2232
|
+
databaseName,
|
|
2233
|
+
schemaName,
|
|
2234
|
+
tableName,
|
|
2235
|
+
message: formattedRelationshipMessage
|
|
2236
|
+
};
|
|
2237
|
+
await hook.writeSchema(input);
|
|
2238
|
+
}, { concurrency: 3 });
|
|
2239
|
+
} else {
|
|
2240
|
+
logger.info(`[DRY_RUN] Stream: ${streamId} - Skipping warehouse schema update and schema hooks`);
|
|
2241
|
+
}
|
|
2061
2242
|
return Promise.resolve();
|
|
2062
2243
|
} catch (err) {
|
|
2063
2244
|
logger.error(`Error when handling schema event.`, err);
|
|
@@ -2078,6 +2259,29 @@ var ITarget = class _ITarget {
|
|
|
2078
2259
|
};
|
|
2079
2260
|
complete = async () => {
|
|
2080
2261
|
try {
|
|
2262
|
+
if (isDryRun()) {
|
|
2263
|
+
logger.info(`[DRY_RUN] Data Extraction is complete. Copying tmp files to out/ instead of loading to warehouse.`);
|
|
2264
|
+
const outDir = path2.resolve("out");
|
|
2265
|
+
await fs4.ensureDir(outDir);
|
|
2266
|
+
for (const streamId of Object.keys(this.streams)) {
|
|
2267
|
+
const streamState = this.streams[streamId];
|
|
2268
|
+
if (!streamState || streamState.getBatchedRowCount() === 0) continue;
|
|
2269
|
+
const fileToLoad = streamState.getFileToLoad();
|
|
2270
|
+
await new Promise((resolve, reject) => {
|
|
2271
|
+
fileToLoad.stream.end(() => {
|
|
2272
|
+
fileToLoad.stream.close((err) => {
|
|
2273
|
+
if (err) reject(err);
|
|
2274
|
+
else resolve();
|
|
2275
|
+
});
|
|
2276
|
+
});
|
|
2277
|
+
});
|
|
2278
|
+
const destPath = path2.join(outDir, `${safePath(streamId)}.ndjson`);
|
|
2279
|
+
await fs4.copy(fileToLoad.path, destPath);
|
|
2280
|
+
logger.info(`[DRY_RUN] Stream: ${streamId} - Copied ${streamState.getBatchedRowCount()} records to ${destPath}`);
|
|
2281
|
+
await fs4.remove(fileToLoad.path);
|
|
2282
|
+
}
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2081
2285
|
logger.info(`\u{1F44D} Data Extraction is complete. Will load remaining batched stream records.`);
|
|
2082
2286
|
await this.loadAllStreamsInWarehouse({ isFinalLoad: true });
|
|
2083
2287
|
return Promise.resolve();
|
|
@@ -2216,7 +2420,7 @@ function extractPrimaryKeysFromExcelFields(fields) {
|
|
|
2216
2420
|
|
|
2217
2421
|
// src/sdk/file-processing/excel/reader.ts
|
|
2218
2422
|
import XLSX from "xlsx";
|
|
2219
|
-
import
|
|
2423
|
+
import path3 from "path";
|
|
2220
2424
|
var excelColumnToIndex = (columnLetter) => {
|
|
2221
2425
|
const letters = columnLetter.toUpperCase();
|
|
2222
2426
|
let index = 0;
|
|
@@ -2375,7 +2579,7 @@ var validateExcelConfig = (config) => {
|
|
|
2375
2579
|
}
|
|
2376
2580
|
};
|
|
2377
2581
|
var findAllMatchingConfigs = (filename, configs) => {
|
|
2378
|
-
const fileInfo =
|
|
2582
|
+
const fileInfo = path3.parse(filename);
|
|
2379
2583
|
return configs.filter(
|
|
2380
2584
|
(config) => config.fileNameValidator(fileInfo.base) && config.extension === fileInfo.ext.replace(".", "")
|
|
2381
2585
|
);
|
|
@@ -2510,7 +2714,7 @@ var formatHexComparison = (expected, actual) => {
|
|
|
2510
2714
|
Expected: "${expected}" (${expectedInfo.length} chars) [${expectedInfo.hex}]
|
|
2511
2715
|
Actual: "${actual}" (${actualInfo.length} chars) [${actualInfo.hex}]`;
|
|
2512
2716
|
};
|
|
2513
|
-
async function* rowGeneratorFromCsv(
|
|
2717
|
+
async function* rowGeneratorFromCsv(path7, fileConfig) {
|
|
2514
2718
|
const isGeneratorConfigArray = Array.isArray(fileConfig.fields);
|
|
2515
2719
|
const configKeys = isGeneratorConfigArray ? [] : Object.keys(fileConfig.fields);
|
|
2516
2720
|
const csvOptions = { separator: fileConfig.separator };
|
|
@@ -2522,10 +2726,10 @@ async function* rowGeneratorFromCsv(path2, fileConfig) {
|
|
|
2522
2726
|
let finalInputStream;
|
|
2523
2727
|
const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
|
|
2524
2728
|
if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
|
|
2525
|
-
initialReadStream = createReadStream(
|
|
2729
|
+
initialReadStream = createReadStream(path7);
|
|
2526
2730
|
finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
|
|
2527
2731
|
} else {
|
|
2528
|
-
initialReadStream = createReadStream(
|
|
2732
|
+
initialReadStream = createReadStream(path7);
|
|
2529
2733
|
finalInputStream = initialReadStream.pipe(createStripBomTransform());
|
|
2530
2734
|
}
|
|
2531
2735
|
finalInputStream.pipe(csvStream);
|
|
@@ -2567,7 +2771,7 @@ async function countCsvLines(filePath) {
|
|
|
2567
2771
|
}).on("end", () => resolve(Math.max(0, newlines - 1))).on("error", reject);
|
|
2568
2772
|
});
|
|
2569
2773
|
}
|
|
2570
|
-
var checkCsvHeaderRow = async (
|
|
2774
|
+
var checkCsvHeaderRow = async (path7, fileConfig) => {
|
|
2571
2775
|
return new Promise((resolve, reject) => {
|
|
2572
2776
|
const colsWithParsingConfig = Object.keys(fileConfig.fields).map((key) => key.trim());
|
|
2573
2777
|
console.debug(`${logPrefix} CSV Expected columns:`, colsWithParsingConfig.map((col) => {
|
|
@@ -2584,10 +2788,10 @@ var checkCsvHeaderRow = async (path2, fileConfig) => {
|
|
|
2584
2788
|
let finalInputStream;
|
|
2585
2789
|
const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
|
|
2586
2790
|
if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
|
|
2587
|
-
initialReadStream = createReadStream(
|
|
2791
|
+
initialReadStream = createReadStream(path7);
|
|
2588
2792
|
finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
|
|
2589
2793
|
} else {
|
|
2590
|
-
initialReadStream = createReadStream(
|
|
2794
|
+
initialReadStream = createReadStream(path7);
|
|
2591
2795
|
finalInputStream = initialReadStream.pipe(createStripBomTransform());
|
|
2592
2796
|
}
|
|
2593
2797
|
let isFirstLine = true;
|
|
@@ -2613,7 +2817,7 @@ var checkCsvHeaderRow = async (path2, fileConfig) => {
|
|
|
2613
2817
|
i,
|
|
2614
2818
|
col,
|
|
2615
2819
|
headers[i] || "<undefined>",
|
|
2616
|
-
|
|
2820
|
+
path7
|
|
2617
2821
|
));
|
|
2618
2822
|
}
|
|
2619
2823
|
});
|
|
@@ -2630,7 +2834,7 @@ var checkCsvHeaderRow = async (path2, fileConfig) => {
|
|
|
2630
2834
|
throw new Error(format4(
|
|
2631
2835
|
`CSV missing expected column '%s' in file: %s`,
|
|
2632
2836
|
col,
|
|
2633
|
-
|
|
2837
|
+
path7
|
|
2634
2838
|
));
|
|
2635
2839
|
}
|
|
2636
2840
|
});
|
|
@@ -2656,7 +2860,7 @@ var writeDataToCsv = async (fileName, data) => {
|
|
|
2656
2860
|
try {
|
|
2657
2861
|
const firstRow = data[0];
|
|
2658
2862
|
if (!firstRow) {
|
|
2659
|
-
|
|
2863
|
+
logger.info(`${logPrefix2} No data to write to CSV file=%s`, fileName);
|
|
2660
2864
|
return;
|
|
2661
2865
|
}
|
|
2662
2866
|
const csvWriter = createObjectCsvWriter({
|
|
@@ -2666,7 +2870,7 @@ var writeDataToCsv = async (fileName, data) => {
|
|
|
2666
2870
|
})
|
|
2667
2871
|
});
|
|
2668
2872
|
await csvWriter.writeRecords(data);
|
|
2669
|
-
|
|
2873
|
+
logger.info(`${logPrefix2} CSV file=%s written successfully`, fileName);
|
|
2670
2874
|
} catch (err) {
|
|
2671
2875
|
if (err instanceof Error) {
|
|
2672
2876
|
throw new Error(format5(`error while writing csv file=%s, err: %s`, fileName, err.message));
|
|
@@ -2674,15 +2878,17 @@ var writeDataToCsv = async (fileName, data) => {
|
|
|
2674
2878
|
throw err;
|
|
2675
2879
|
}
|
|
2676
2880
|
};
|
|
2677
|
-
var writeGeneratorToCSV = async (generator, outputFileName) => {
|
|
2881
|
+
var writeGeneratorToCSV = async (generator, outputFileName, options) => {
|
|
2882
|
+
const batchSize = options?.batchSize ?? 1e4;
|
|
2883
|
+
const alwaysQuote = options?.alwaysQuote ?? true;
|
|
2678
2884
|
try {
|
|
2679
2885
|
let rowCount = 0;
|
|
2680
2886
|
const firstDataRow = (await generator.next()).value;
|
|
2681
2887
|
if (!firstDataRow) {
|
|
2682
|
-
|
|
2888
|
+
logger.info(`${logPrefix2} [%s] first data row of csv empty, skipping`, outputFileName);
|
|
2683
2889
|
return 0;
|
|
2684
2890
|
}
|
|
2685
|
-
|
|
2891
|
+
logger.info(`${logPrefix2} [%s] inferring headers from first data row`, outputFileName);
|
|
2686
2892
|
const headers = Object.keys(firstDataRow).map((col) => {
|
|
2687
2893
|
return {
|
|
2688
2894
|
id: col,
|
|
@@ -2692,14 +2898,14 @@ var writeGeneratorToCSV = async (generator, outputFileName) => {
|
|
|
2692
2898
|
const csvWriter = createObjectCsvWriter({
|
|
2693
2899
|
path: outputFileName,
|
|
2694
2900
|
header: headers,
|
|
2695
|
-
alwaysQuote
|
|
2901
|
+
alwaysQuote
|
|
2696
2902
|
});
|
|
2697
|
-
|
|
2903
|
+
logger.info(`${logPrefix2} [%s] writing csv`, outputFileName);
|
|
2698
2904
|
let batch = [firstDataRow];
|
|
2699
2905
|
for await (const data of generator) {
|
|
2700
2906
|
batch.push(data);
|
|
2701
2907
|
rowCount++;
|
|
2702
|
-
if (batch.length
|
|
2908
|
+
if (batch.length >= batchSize) {
|
|
2703
2909
|
try {
|
|
2704
2910
|
await csvWriter.writeRecords(batch);
|
|
2705
2911
|
} catch (err) {
|
|
@@ -2717,8 +2923,10 @@ var writeGeneratorToCSV = async (generator, outputFileName) => {
|
|
|
2717
2923
|
batch = [];
|
|
2718
2924
|
}
|
|
2719
2925
|
}
|
|
2720
|
-
|
|
2721
|
-
|
|
2926
|
+
if (batch.length > 0) {
|
|
2927
|
+
await csvWriter.writeRecords(batch);
|
|
2928
|
+
}
|
|
2929
|
+
logger.info(`${logPrefix2} [%s] csv written successfully with %d rows`, outputFileName, rowCount);
|
|
2722
2930
|
return rowCount;
|
|
2723
2931
|
} catch (err) {
|
|
2724
2932
|
if (err instanceof Error) {
|
|
@@ -3004,9 +3212,65 @@ var FilePatterns = class {
|
|
|
3004
3212
|
};
|
|
3005
3213
|
|
|
3006
3214
|
// src/services/sftp.ts
|
|
3215
|
+
import path4 from "path";
|
|
3216
|
+
import fs5 from "fs-extra";
|
|
3007
3217
|
import Client from "ssh2-sftp-client";
|
|
3008
3218
|
import { default as default2 } from "ssh2-sftp-client";
|
|
3009
3219
|
var SftpService = new Client();
|
|
3220
|
+
var logPrefix3 = "[sftp]";
|
|
3221
|
+
async function listFilesRecursively(client, remotePath, options) {
|
|
3222
|
+
const recursive = options?.recursive ?? true;
|
|
3223
|
+
const extensions = options?.extensions?.map((e) => e.toLowerCase());
|
|
3224
|
+
const maxIterations = options?.maxIterations ?? 1e3;
|
|
3225
|
+
const results = [];
|
|
3226
|
+
const dirs = [remotePath];
|
|
3227
|
+
let iterations = 0;
|
|
3228
|
+
while (dirs.length > 0) {
|
|
3229
|
+
if (iterations++ >= maxIterations) {
|
|
3230
|
+
logger.warn(`${logPrefix3} listFilesRecursively reached maxIterations (${maxIterations}), stopping.`);
|
|
3231
|
+
break;
|
|
3232
|
+
}
|
|
3233
|
+
const currentDir = dirs.shift();
|
|
3234
|
+
let entries;
|
|
3235
|
+
try {
|
|
3236
|
+
entries = await client.list(currentDir);
|
|
3237
|
+
} catch (err) {
|
|
3238
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3239
|
+
logger.warn(`${logPrefix3} Failed to list ${currentDir}: ${msg}`);
|
|
3240
|
+
continue;
|
|
3241
|
+
}
|
|
3242
|
+
for (const entry of entries) {
|
|
3243
|
+
if (entry.type === "d") {
|
|
3244
|
+
if (recursive && entry.name !== "." && entry.name !== "..") {
|
|
3245
|
+
dirs.push(path4.posix.join(currentDir, entry.name));
|
|
3246
|
+
}
|
|
3247
|
+
continue;
|
|
3248
|
+
}
|
|
3249
|
+
if (entry.type !== "-") continue;
|
|
3250
|
+
if (extensions) {
|
|
3251
|
+
const ext = path4.extname(entry.name).toLowerCase();
|
|
3252
|
+
if (!extensions.includes(ext)) continue;
|
|
3253
|
+
}
|
|
3254
|
+
const fullPath = path4.posix.join(currentDir, entry.name);
|
|
3255
|
+
results.push({ ...entry, name: fullPath });
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
logger.info(`${logPrefix3} listFilesRecursively found ${results.length} files under ${remotePath}`);
|
|
3259
|
+
return results;
|
|
3260
|
+
}
|
|
3261
|
+
async function downloadFiles(client, files, localDir, options) {
|
|
3262
|
+
const concurrency = options?.concurrency ?? 5;
|
|
3263
|
+
await fs5.ensureDir(localDir);
|
|
3264
|
+
const tasks = files.map((file, index) => () => {
|
|
3265
|
+
const localName = `${index}_${path4.basename(file.name)}`;
|
|
3266
|
+
const localPath = path4.join(localDir, localName);
|
|
3267
|
+
logger.debug(`${logPrefix3} Downloading ${file.name} \u2192 ${localPath}`);
|
|
3268
|
+
return client.fastGet(file.name, localPath).then(() => localPath);
|
|
3269
|
+
});
|
|
3270
|
+
const results = await runWithConcurrency(tasks, concurrency);
|
|
3271
|
+
logger.info(`${logPrefix3} Downloaded ${results.length} files to ${localDir}`);
|
|
3272
|
+
return results;
|
|
3273
|
+
}
|
|
3010
3274
|
|
|
3011
3275
|
// src/services/cloud-storage.ts
|
|
3012
3276
|
import { Storage } from "@google-cloud/storage";
|
|
@@ -3014,7 +3278,7 @@ import { format as format6 } from "util";
|
|
|
3014
3278
|
import * as pathModule from "path";
|
|
3015
3279
|
import { existsSync, mkdirSync, unlinkSync as unlinkSync2 } from "fs";
|
|
3016
3280
|
import { randomUUID } from "crypto";
|
|
3017
|
-
var
|
|
3281
|
+
var logPrefix4 = "[CloudStorageService]";
|
|
3018
3282
|
var tmpDir = "tmp";
|
|
3019
3283
|
var CloudStorageService = class {
|
|
3020
3284
|
storage;
|
|
@@ -3022,10 +3286,10 @@ var CloudStorageService = class {
|
|
|
3022
3286
|
processedSuffix;
|
|
3023
3287
|
supportedExtensions;
|
|
3024
3288
|
path;
|
|
3025
|
-
constructor(bucketName,
|
|
3289
|
+
constructor(bucketName, path7, opts) {
|
|
3026
3290
|
this.storage = new Storage({ retryOptions: { autoRetry: true, maxRetries: 20 } });
|
|
3027
3291
|
this.bucket = this.storage.bucket(bucketName);
|
|
3028
|
-
this.path =
|
|
3292
|
+
this.path = path7;
|
|
3029
3293
|
this.processedSuffix = opts?.processedSuffix ?? ".processed";
|
|
3030
3294
|
this.supportedExtensions = opts?.supportedExtensions ?? [];
|
|
3031
3295
|
}
|
|
@@ -3066,7 +3330,7 @@ var CloudStorageService = class {
|
|
|
3066
3330
|
try {
|
|
3067
3331
|
const file = this.bucket.file(markerFileName);
|
|
3068
3332
|
await file.save(format6("Marked file %s as processed", fileName));
|
|
3069
|
-
logger.info(`${
|
|
3333
|
+
logger.info(`${logPrefix4} Marker file ${markerFileName} created successfully.`);
|
|
3070
3334
|
} catch (err) {
|
|
3071
3335
|
if (err instanceof Error) {
|
|
3072
3336
|
throw new Error(format6(`error while writing marker file=%s, err:%s`, markerFileName, err.message));
|
|
@@ -3090,7 +3354,7 @@ var CloudStorageService = class {
|
|
|
3090
3354
|
unlinkSync2(destFilename);
|
|
3091
3355
|
}
|
|
3092
3356
|
await file.download({ destination: destFilename });
|
|
3093
|
-
logger.info(`${
|
|
3357
|
+
logger.info(`${logPrefix4} Downloaded ${fileName} to ${destFilename}`);
|
|
3094
3358
|
return destFilename;
|
|
3095
3359
|
} catch (err) {
|
|
3096
3360
|
if (err instanceof Error) {
|
|
@@ -3108,9 +3372,9 @@ var CloudStorageService = class {
|
|
|
3108
3372
|
*/
|
|
3109
3373
|
async uploadFile(localPath, destPath) {
|
|
3110
3374
|
try {
|
|
3111
|
-
logger.info(`${
|
|
3375
|
+
logger.info(`${logPrefix4} preparing to upload '%s' to '%s'`, localPath, destPath);
|
|
3112
3376
|
const [file] = await this.bucket.upload(localPath, { destination: destPath });
|
|
3113
|
-
logger.info(`${
|
|
3377
|
+
logger.info(`${logPrefix4} file %s has been successfully uploaded into %s`, localPath, destPath);
|
|
3114
3378
|
return file;
|
|
3115
3379
|
} catch (err) {
|
|
3116
3380
|
if (err instanceof Error) {
|
|
@@ -3186,6 +3450,376 @@ var unzip = async (zipFilePath, extractedPath) => {
|
|
|
3186
3450
|
return extractedPath;
|
|
3187
3451
|
};
|
|
3188
3452
|
|
|
3453
|
+
// src/services/cdn.ts
|
|
3454
|
+
import axios3 from "axios";
|
|
3455
|
+
import axiosRetry2 from "axios-retry";
|
|
3456
|
+
var logPrefix5 = "[CdnService]";
|
|
3457
|
+
var MAX_RETRIES = 10;
|
|
3458
|
+
var CdnService = class {
|
|
3459
|
+
axiosClient;
|
|
3460
|
+
constructor(config) {
|
|
3461
|
+
const resolvedKey = getServiceAccountKey(config.serviceAccountKey);
|
|
3462
|
+
const resolvedEndpoint = getApiEndpoint(config.apiEndpoint);
|
|
3463
|
+
this.axiosClient = axios3.create({
|
|
3464
|
+
baseURL: resolvedEndpoint,
|
|
3465
|
+
timeout: 12e4,
|
|
3466
|
+
headers: {
|
|
3467
|
+
Authorization: `Bearer ${resolvedKey}`,
|
|
3468
|
+
Accept: "application/json"
|
|
3469
|
+
}
|
|
3470
|
+
});
|
|
3471
|
+
axiosRetry2(this.axiosClient, {
|
|
3472
|
+
retries: MAX_RETRIES,
|
|
3473
|
+
retryCondition: (error) => {
|
|
3474
|
+
const status = error.response?.status ?? 0;
|
|
3475
|
+
return !error.response || status === 429 || status >= 500 && status < 600;
|
|
3476
|
+
},
|
|
3477
|
+
retryDelay: (retryCount, error) => {
|
|
3478
|
+
const retryAfter = error.response?.headers?.["retry-after"];
|
|
3479
|
+
if (retryAfter) {
|
|
3480
|
+
const seconds = Number(retryAfter);
|
|
3481
|
+
if (!isNaN(seconds) && seconds > 0) {
|
|
3482
|
+
logger.info(`${logPrefix5} Rate limited \u2014 waiting ${seconds}s (Retry-After header), attempt ${retryCount}/${MAX_RETRIES}`);
|
|
3483
|
+
return seconds * 1e3;
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
const delay = axiosRetry2.exponentialDelay(retryCount);
|
|
3487
|
+
logger.info(`${logPrefix5} Retrying in ${delay}ms (attempt ${retryCount}/${MAX_RETRIES}) for ${error.config?.method?.toUpperCase()} ${error.config?.url}`);
|
|
3488
|
+
return delay;
|
|
3489
|
+
}
|
|
3490
|
+
});
|
|
3491
|
+
}
|
|
3492
|
+
/**
|
|
3493
|
+
* Returns metadata for a CDN file via a HEAD request.
|
|
3494
|
+
* If the file does not exist, returns `{ exists: false, lastModified: null, contentType: null }`.
|
|
3495
|
+
* Falls back to not-existing if HEAD is not supported (405).
|
|
3496
|
+
*/
|
|
3497
|
+
async getFileMetadata(cdnId, fileName) {
|
|
3498
|
+
const url = `/v1/cdn/${cdnId}/files/${encodeURIComponent(fileName)}`;
|
|
3499
|
+
try {
|
|
3500
|
+
const response = await this.axiosClient.head(url);
|
|
3501
|
+
const lastModifiedHeader = response.headers["last-modified"];
|
|
3502
|
+
const contentTypeHeader = response.headers["content-type"];
|
|
3503
|
+
return {
|
|
3504
|
+
exists: true,
|
|
3505
|
+
lastModified: lastModifiedHeader ? new Date(lastModifiedHeader) : null,
|
|
3506
|
+
contentType: contentTypeHeader ?? null
|
|
3507
|
+
};
|
|
3508
|
+
} catch (err) {
|
|
3509
|
+
if (axios3.isAxiosError(err)) {
|
|
3510
|
+
const status = err.response?.status;
|
|
3511
|
+
if (status === 404) return { exists: false, lastModified: null, contentType: null };
|
|
3512
|
+
if (status === 405) {
|
|
3513
|
+
logger.warn(`${logPrefix5} HEAD not supported for ${url}, treating file as absent`);
|
|
3514
|
+
return { exists: false, lastModified: null, contentType: null };
|
|
3515
|
+
}
|
|
3516
|
+
this.throwMeaningfulError(err);
|
|
3517
|
+
}
|
|
3518
|
+
throw err;
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
/**
|
|
3522
|
+
* Checks whether a file already exists in the CDN via a HEAD request.
|
|
3523
|
+
* Returns true if the server responds 2xx, false on 404.
|
|
3524
|
+
* Falls back to false (treat as not existing) if HEAD is not supported (405).
|
|
3525
|
+
* No body is transferred.
|
|
3526
|
+
*/
|
|
3527
|
+
async fileExists(cdnId, fileName) {
|
|
3528
|
+
return (await this.getFileMetadata(cdnId, fileName)).exists;
|
|
3529
|
+
}
|
|
3530
|
+
/**
|
|
3531
|
+
* Uploads a file buffer to the Whaly CDN.
|
|
3532
|
+
* The PUT is idempotent — uploading the same fileName overwrites the existing file.
|
|
3533
|
+
*/
|
|
3534
|
+
async uploadFile(cdnId, fileName, fileBuffer) {
|
|
3535
|
+
const url = `/v1/cdn/${cdnId}/files/${encodeURIComponent(fileName)}`;
|
|
3536
|
+
try {
|
|
3537
|
+
logger.info(`${logPrefix5} Uploading ${fileName} (${fileBuffer.length} bytes) to CDN ${cdnId}`);
|
|
3538
|
+
const form = new FormData();
|
|
3539
|
+
form.append("file", new Blob([fileBuffer]), fileName);
|
|
3540
|
+
const response = await this.axiosClient.put(url, form);
|
|
3541
|
+
logger.info(`${logPrefix5} Successfully uploaded ${fileName} (status ${response.status})`);
|
|
3542
|
+
return {
|
|
3543
|
+
filePath: response.data?.filePath ?? `/org/${cdnId}/file/${fileName}`
|
|
3544
|
+
};
|
|
3545
|
+
} catch (err) {
|
|
3546
|
+
if (axios3.isAxiosError(err)) {
|
|
3547
|
+
this.throwMeaningfulError(err);
|
|
3548
|
+
}
|
|
3549
|
+
throw err;
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
throwMeaningfulError(err) {
|
|
3553
|
+
const status = err.response?.status;
|
|
3554
|
+
if (status === 401 || status === 403) {
|
|
3555
|
+
throw new Error("Unauthorized - invalid or expired service account key");
|
|
3556
|
+
} else if (status === 404) {
|
|
3557
|
+
throw new Error("Organization or CDN not found");
|
|
3558
|
+
} else if (status === 500) {
|
|
3559
|
+
throw new Error("API server error - check CDN ID and service account permissions");
|
|
3560
|
+
} else {
|
|
3561
|
+
throw new Error(`API error: ${err.response?.statusText ?? err.message}`);
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
};
|
|
3565
|
+
|
|
3566
|
+
// src/sdk/models/asset-tap/asset-stream.ts
|
|
3567
|
+
var AssetStream = class {
|
|
3568
|
+
streamId;
|
|
3569
|
+
config;
|
|
3570
|
+
replicationMode;
|
|
3571
|
+
constructor(streamId, config, replicationMode = "INCREMENTAL") {
|
|
3572
|
+
this.streamId = streamId;
|
|
3573
|
+
this.config = config;
|
|
3574
|
+
this.replicationMode = replicationMode;
|
|
3575
|
+
}
|
|
3576
|
+
async transformFile(downloadedPath, _entry) {
|
|
3577
|
+
return downloadedPath;
|
|
3578
|
+
}
|
|
3579
|
+
};
|
|
3580
|
+
|
|
3581
|
+
// src/sdk/models/asset-tap/asset-tap.ts
|
|
3582
|
+
import fs6 from "fs-extra";
|
|
3583
|
+
import path5 from "path";
|
|
3584
|
+
var logPrefix6 = "[AssetTap]";
|
|
3585
|
+
function inferContentType(filePath, fallback) {
|
|
3586
|
+
const mime = getMimeType(filePath);
|
|
3587
|
+
return mime !== "application/octet-stream" ? mime : fallback;
|
|
3588
|
+
}
|
|
3589
|
+
function deriveManifestMode(streams) {
|
|
3590
|
+
const modes = new Set(streams.map((s) => s.replicationMode));
|
|
3591
|
+
if (modes.size > 1) {
|
|
3592
|
+
logger.warn(`${logPrefix6} Streams have mixed replication modes (${[...modes].join(", ")}). Manifest will report "FULL".`);
|
|
3593
|
+
return "FULL";
|
|
3594
|
+
}
|
|
3595
|
+
return streams[0]?.replicationMode ?? "INCREMENTAL";
|
|
3596
|
+
}
|
|
3597
|
+
var AssetTap = class {
|
|
3598
|
+
config;
|
|
3599
|
+
outputDir;
|
|
3600
|
+
streams = [];
|
|
3601
|
+
concurrency;
|
|
3602
|
+
target;
|
|
3603
|
+
constructor(target, config, outputDir = "out", concurrency = 5) {
|
|
3604
|
+
this.target = target;
|
|
3605
|
+
this.config = config;
|
|
3606
|
+
this.outputDir = outputDir;
|
|
3607
|
+
this.concurrency = concurrency;
|
|
3608
|
+
}
|
|
3609
|
+
async sync() {
|
|
3610
|
+
await this.init();
|
|
3611
|
+
const dryRun = isDryRun();
|
|
3612
|
+
const dryRunLimit = dryRun ? getDryRunLimit() : void 0;
|
|
3613
|
+
if (dryRun) {
|
|
3614
|
+
logger.info(`${logPrefix6} [DRY_RUN] mode active \u2014 skipping CDN checks and uploads`);
|
|
3615
|
+
await fs6.emptyDir(this.outputDir);
|
|
3616
|
+
if (dryRunLimit !== void 0) {
|
|
3617
|
+
logger.info(`${logPrefix6} [DRY_RUN] Limit: ${dryRunLimit} assets per stream`);
|
|
3618
|
+
}
|
|
3619
|
+
} else if (process.env["DRY_RUN_LIMIT"] !== void 0) {
|
|
3620
|
+
logger.warn(`${logPrefix6} DRY_RUN_LIMIT is set but DRY_RUN is not active \u2014 limit will be ignored`);
|
|
3621
|
+
}
|
|
3622
|
+
const tmpDir2 = path5.join(this.outputDir, "tmp");
|
|
3623
|
+
await fs6.ensureDir(tmpDir2);
|
|
3624
|
+
const streamManifests = [];
|
|
3625
|
+
let entryIndex = 0;
|
|
3626
|
+
let totalSummary = { total: 0, uploaded: 0, skipped: 0, errors: 0 };
|
|
3627
|
+
for (const stream of this.streams) {
|
|
3628
|
+
logger.info(`${logPrefix6} Processing stream: ${stream.streamId} (mode=${stream.replicationMode}, concurrency=${this.concurrency})`);
|
|
3629
|
+
const assetEntries = [];
|
|
3630
|
+
let streamAssetCount = 0;
|
|
3631
|
+
await processFromAsyncIterable(
|
|
3632
|
+
stream.listAssets(),
|
|
3633
|
+
async (entry) => {
|
|
3634
|
+
if (dryRunLimit !== void 0 && streamAssetCount >= dryRunLimit) {
|
|
3635
|
+
return "stop";
|
|
3636
|
+
}
|
|
3637
|
+
logger.debug(`${logPrefix6} Processing entry: ${entry.sourcePath}`);
|
|
3638
|
+
if (stream.replicationMode === "INCREMENTAL" && !dryRun) {
|
|
3639
|
+
const shouldSync = await this.target.shouldSync(entry);
|
|
3640
|
+
if (!shouldSync) {
|
|
3641
|
+
logger.debug(`${logPrefix6} Skipping ${entry.sourcePath} (up-to-date)`);
|
|
3642
|
+
assetEntries.push({
|
|
3643
|
+
sourcePath: entry.sourcePath,
|
|
3644
|
+
destinationPath: entry.destinationPath,
|
|
3645
|
+
downloadedPath: "",
|
|
3646
|
+
transformedPath: "",
|
|
3647
|
+
size: 0,
|
|
3648
|
+
contentType: entry.contentType,
|
|
3649
|
+
status: "skipped",
|
|
3650
|
+
transformed: false
|
|
3651
|
+
});
|
|
3652
|
+
return "continue";
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
const fileName = `${entryIndex}_${path5.basename(entry.sourcePath)}`;
|
|
3656
|
+
const downloadedPath = path5.join(tmpDir2, fileName);
|
|
3657
|
+
let uploadPath = downloadedPath;
|
|
3658
|
+
entryIndex++;
|
|
3659
|
+
streamAssetCount++;
|
|
3660
|
+
try {
|
|
3661
|
+
await stream.downloadEntry(entry, downloadedPath);
|
|
3662
|
+
uploadPath = await stream.transformFile(downloadedPath, entry);
|
|
3663
|
+
const wasTransformed = uploadPath !== downloadedPath;
|
|
3664
|
+
const stat = await fs6.stat(uploadPath);
|
|
3665
|
+
const processed = {
|
|
3666
|
+
entry,
|
|
3667
|
+
downloadedPath,
|
|
3668
|
+
uploadPath,
|
|
3669
|
+
wasTransformed,
|
|
3670
|
+
size: stat.size,
|
|
3671
|
+
contentType: wasTransformed ? inferContentType(uploadPath, entry.contentType) : entry.contentType
|
|
3672
|
+
};
|
|
3673
|
+
if (!dryRun) {
|
|
3674
|
+
await this.target.uploadAsset(processed);
|
|
3675
|
+
} else {
|
|
3676
|
+
const inspectPath = path5.join(this.outputDir, stream.streamId, entry.destinationPath);
|
|
3677
|
+
await fs6.ensureDir(path5.dirname(inspectPath));
|
|
3678
|
+
await fs6.copy(uploadPath, inspectPath);
|
|
3679
|
+
}
|
|
3680
|
+
assetEntries.push({
|
|
3681
|
+
sourcePath: entry.sourcePath,
|
|
3682
|
+
destinationPath: entry.destinationPath,
|
|
3683
|
+
downloadedPath,
|
|
3684
|
+
transformedPath: wasTransformed ? uploadPath : "",
|
|
3685
|
+
size: processed.size,
|
|
3686
|
+
contentType: processed.contentType,
|
|
3687
|
+
status: "uploaded",
|
|
3688
|
+
transformed: wasTransformed
|
|
3689
|
+
});
|
|
3690
|
+
logger.info(`${logPrefix6} ${dryRun ? "[DRY_RUN] Processed" : "Uploaded"} ${entry.sourcePath} \u2192 ${entry.destinationPath}`);
|
|
3691
|
+
} catch (err) {
|
|
3692
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3693
|
+
logger.error(`${logPrefix6} Failed to process ${entry.sourcePath}: ${message}`);
|
|
3694
|
+
assetEntries.push({
|
|
3695
|
+
sourcePath: entry.sourcePath,
|
|
3696
|
+
destinationPath: entry.destinationPath,
|
|
3697
|
+
downloadedPath: "",
|
|
3698
|
+
transformedPath: "",
|
|
3699
|
+
size: 0,
|
|
3700
|
+
contentType: entry.contentType,
|
|
3701
|
+
status: "error",
|
|
3702
|
+
transformed: false,
|
|
3703
|
+
error: message
|
|
3704
|
+
});
|
|
3705
|
+
} finally {
|
|
3706
|
+
await fs6.remove(downloadedPath).catch(() => void 0);
|
|
3707
|
+
if (uploadPath !== downloadedPath) {
|
|
3708
|
+
await fs6.remove(uploadPath).catch(() => void 0);
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
if (dryRunLimit !== void 0 && streamAssetCount >= dryRunLimit) {
|
|
3712
|
+
logger.info(`${logPrefix6} [DRY_RUN] Reached limit of ${dryRunLimit} for stream "${stream.streamId}", stopping.`);
|
|
3713
|
+
return "stop";
|
|
3714
|
+
}
|
|
3715
|
+
return "continue";
|
|
3716
|
+
},
|
|
3717
|
+
this.concurrency
|
|
3718
|
+
);
|
|
3719
|
+
const streamSummary = {
|
|
3720
|
+
total: assetEntries.length,
|
|
3721
|
+
uploaded: assetEntries.filter((a) => a.status === "uploaded").length,
|
|
3722
|
+
skipped: assetEntries.filter((a) => a.status === "skipped").length,
|
|
3723
|
+
errors: assetEntries.filter((a) => a.status === "error").length
|
|
3724
|
+
};
|
|
3725
|
+
totalSummary.total += streamSummary.total;
|
|
3726
|
+
totalSummary.uploaded += streamSummary.uploaded;
|
|
3727
|
+
totalSummary.skipped += streamSummary.skipped;
|
|
3728
|
+
totalSummary.errors += streamSummary.errors;
|
|
3729
|
+
streamManifests.push({
|
|
3730
|
+
streamId: stream.streamId,
|
|
3731
|
+
mode: stream.replicationMode,
|
|
3732
|
+
syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3733
|
+
assets: assetEntries,
|
|
3734
|
+
summary: streamSummary
|
|
3735
|
+
});
|
|
3736
|
+
}
|
|
3737
|
+
const manifest = {
|
|
3738
|
+
syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3739
|
+
mode: deriveManifestMode(this.streams),
|
|
3740
|
+
streams: streamManifests,
|
|
3741
|
+
summary: totalSummary
|
|
3742
|
+
};
|
|
3743
|
+
await fs6.ensureDir(this.outputDir);
|
|
3744
|
+
await fs6.writeJson(path5.join(this.outputDir, "manifest.json"), manifest, { spaces: 2 });
|
|
3745
|
+
if (!dryRun) {
|
|
3746
|
+
await this.target.complete();
|
|
3747
|
+
}
|
|
3748
|
+
logger.info(`${logPrefix6} Sync complete. Uploaded=${totalSummary.uploaded} Skipped=${totalSummary.skipped} Errors=${totalSummary.errors}`);
|
|
3749
|
+
return manifest;
|
|
3750
|
+
}
|
|
3751
|
+
};
|
|
3752
|
+
|
|
3753
|
+
// src/sdk/models/asset-tap/image-transform.ts
|
|
3754
|
+
import sharp from "sharp";
|
|
3755
|
+
import path6 from "path";
|
|
3756
|
+
var SHARP_GRAVITY = {
|
|
3757
|
+
center: "centre",
|
|
3758
|
+
top: "north",
|
|
3759
|
+
bottom: "south",
|
|
3760
|
+
left: "west",
|
|
3761
|
+
right: "east",
|
|
3762
|
+
"top left": "northwest",
|
|
3763
|
+
"top right": "northeast",
|
|
3764
|
+
"bottom left": "southwest",
|
|
3765
|
+
"bottom right": "southeast"
|
|
3766
|
+
};
|
|
3767
|
+
function parseBackground(bg) {
|
|
3768
|
+
if (bg === void 0 || bg === "none" || bg === "transparent") {
|
|
3769
|
+
return { r: 0, g: 0, b: 0, alpha: 0 };
|
|
3770
|
+
}
|
|
3771
|
+
return bg;
|
|
3772
|
+
}
|
|
3773
|
+
var ImageTransform = class {
|
|
3774
|
+
/**
|
|
3775
|
+
* Converts an image to WebP format using sharp (libvips).
|
|
3776
|
+
*
|
|
3777
|
+
* The output file is written to the same directory as the input,
|
|
3778
|
+
* with the extension replaced by `.webp`.
|
|
3779
|
+
*
|
|
3780
|
+
* @param inputPath Path to the source image file.
|
|
3781
|
+
* @param options Optional resize / padding parameters.
|
|
3782
|
+
* @returns Path to the produced `.webp` file.
|
|
3783
|
+
*/
|
|
3784
|
+
static async toWebp(inputPath, options) {
|
|
3785
|
+
const dir = path6.dirname(inputPath);
|
|
3786
|
+
const basename = path6.basename(inputPath, path6.extname(inputPath));
|
|
3787
|
+
const outputPath = path6.join(dir, `${basename}.webp`);
|
|
3788
|
+
let pipeline = sharp(inputPath);
|
|
3789
|
+
const hasSize = options.width !== void 0 && options.height !== void 0;
|
|
3790
|
+
if (hasSize) {
|
|
3791
|
+
const position = options.gravity !== void 0 ? SHARP_GRAVITY[options.gravity] ?? options.gravity : void 0;
|
|
3792
|
+
if (options.extent === true) {
|
|
3793
|
+
pipeline = pipeline.resize(options.width, options.height, {
|
|
3794
|
+
fit: "contain",
|
|
3795
|
+
position,
|
|
3796
|
+
background: parseBackground(options.background)
|
|
3797
|
+
});
|
|
3798
|
+
} else {
|
|
3799
|
+
pipeline = pipeline.resize(options.width, options.height, {
|
|
3800
|
+
fit: "inside",
|
|
3801
|
+
position
|
|
3802
|
+
});
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
pipeline = pipeline.webp(
|
|
3806
|
+
options.quality !== void 0 ? { quality: options.quality } : {}
|
|
3807
|
+
);
|
|
3808
|
+
await pipeline.toFile(outputPath);
|
|
3809
|
+
return outputPath;
|
|
3810
|
+
}
|
|
3811
|
+
};
|
|
3812
|
+
|
|
3813
|
+
// src/sdk/models/asset-target/asset-target.ts
|
|
3814
|
+
var AssetTarget = class {
|
|
3815
|
+
config;
|
|
3816
|
+
constructor(config) {
|
|
3817
|
+
this.config = config;
|
|
3818
|
+
}
|
|
3819
|
+
async complete() {
|
|
3820
|
+
}
|
|
3821
|
+
};
|
|
3822
|
+
|
|
3189
3823
|
// src/targets/bigquery/helpers.ts
|
|
3190
3824
|
var safeColumnName = (key) => {
|
|
3191
3825
|
let returnString = key;
|
|
@@ -3222,7 +3856,9 @@ import Decimal from "decimal.js";
|
|
|
3222
3856
|
import dayjs5 from "dayjs";
|
|
3223
3857
|
var validateDateRange = (record, schema) => {
|
|
3224
3858
|
Object.keys(schema).forEach((key) => {
|
|
3225
|
-
const
|
|
3859
|
+
const field = schema[key];
|
|
3860
|
+
if (!field) return;
|
|
3861
|
+
const { format: format8 } = field;
|
|
3226
3862
|
if (format8 === "date-time") {
|
|
3227
3863
|
if (record[key]) {
|
|
3228
3864
|
const formattedDate = dayjs5(record[key]).format(defaultDateTimeFormat);
|
|
@@ -3452,6 +4088,12 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
|
|
|
3452
4088
|
});
|
|
3453
4089
|
};
|
|
3454
4090
|
getReplaceQueries = () => {
|
|
4091
|
+
if (this.primaryKeys.length === 0) {
|
|
4092
|
+
return [
|
|
4093
|
+
`TRUNCATE TABLE ${this.sqlTableId};`,
|
|
4094
|
+
...this.getAppendQueries()
|
|
4095
|
+
];
|
|
4096
|
+
}
|
|
3455
4097
|
return [
|
|
3456
4098
|
`TRUNCATE TABLE ${this.sqlTableId};`,
|
|
3457
4099
|
...this.getMergeQueries()
|
|
@@ -3739,6 +4381,46 @@ var BigQueryTarget = class extends ITarget {
|
|
|
3739
4381
|
convertNumberIntoDecimal = convertNumberIntoDecimal;
|
|
3740
4382
|
};
|
|
3741
4383
|
|
|
4384
|
+
// src/targets/cdn/main.ts
|
|
4385
|
+
import fs7 from "fs-extra";
|
|
4386
|
+
var logPrefix7 = "[CdnAssetTarget]";
|
|
4387
|
+
var CdnAssetTarget = class extends AssetTarget {
|
|
4388
|
+
cdnService;
|
|
4389
|
+
constructor(config) {
|
|
4390
|
+
super(config);
|
|
4391
|
+
this.cdnService = new CdnService(config);
|
|
4392
|
+
}
|
|
4393
|
+
async shouldSync(entry) {
|
|
4394
|
+
const metadata = await this.cdnService.getFileMetadata(getCdnId(this.config.cdnId), entry.destinationPath);
|
|
4395
|
+
if (!metadata.exists) {
|
|
4396
|
+
logger.debug(`${logPrefix7} ${entry.destinationPath} not in CDN \u2192 will sync`);
|
|
4397
|
+
return true;
|
|
4398
|
+
}
|
|
4399
|
+
if (entry.lastModified === void 0) {
|
|
4400
|
+
logger.debug(`${logPrefix7} ${entry.destinationPath} source has no lastModified \u2192 will sync`);
|
|
4401
|
+
return true;
|
|
4402
|
+
}
|
|
4403
|
+
if (metadata.lastModified === null) {
|
|
4404
|
+
logger.debug(`${logPrefix7} ${entry.destinationPath} CDN has no lastModified \u2192 will sync`);
|
|
4405
|
+
return true;
|
|
4406
|
+
}
|
|
4407
|
+
const sourceIsNewer = entry.lastModified > metadata.lastModified;
|
|
4408
|
+
if (!sourceIsNewer) {
|
|
4409
|
+
logger.debug(`${logPrefix7} Skipping ${entry.destinationPath} (CDN is up-to-date)`);
|
|
4410
|
+
}
|
|
4411
|
+
return sourceIsNewer;
|
|
4412
|
+
}
|
|
4413
|
+
async uploadAsset(asset) {
|
|
4414
|
+
const fileBuffer = await fs7.readFile(asset.uploadPath);
|
|
4415
|
+
await this.cdnService.uploadFile(
|
|
4416
|
+
getCdnId(this.config.cdnId),
|
|
4417
|
+
asset.entry.destinationPath,
|
|
4418
|
+
fileBuffer
|
|
4419
|
+
);
|
|
4420
|
+
logger.info(`${logPrefix7} Uploaded ${asset.entry.destinationPath} (${fileBuffer.length} bytes)`);
|
|
4421
|
+
}
|
|
4422
|
+
};
|
|
4423
|
+
|
|
3742
4424
|
// src/state-providers/gcs/main.ts
|
|
3743
4425
|
import { format as format7 } from "util";
|
|
3744
4426
|
var GCSStateProvider = class {
|
|
@@ -3779,9 +4461,14 @@ var GCSStateProvider = class {
|
|
|
3779
4461
|
export {
|
|
3780
4462
|
ALL_STREAM_ID_KEYWORD,
|
|
3781
4463
|
API_CALLS_METRIC_NAME,
|
|
4464
|
+
AssetStream,
|
|
4465
|
+
AssetTap,
|
|
4466
|
+
AssetTarget,
|
|
3782
4467
|
Authenticator,
|
|
3783
4468
|
BATCH_INTERVAL_MS,
|
|
3784
4469
|
BigQueryTarget,
|
|
4470
|
+
CdnAssetTarget,
|
|
4471
|
+
CdnService,
|
|
3785
4472
|
CloudStorageService,
|
|
3786
4473
|
CounterMetric,
|
|
3787
4474
|
CsvExtractionConfigBuilder,
|
|
@@ -3795,6 +4482,8 @@ export {
|
|
|
3795
4482
|
FileTap,
|
|
3796
4483
|
GCSStateProvider,
|
|
3797
4484
|
ITarget,
|
|
4485
|
+
ImageTransform,
|
|
4486
|
+
MIME_TYPES,
|
|
3798
4487
|
MissingFieldInSchemaError,
|
|
3799
4488
|
MissingSchemaError,
|
|
3800
4489
|
RESTStream,
|
|
@@ -3816,6 +4505,7 @@ export {
|
|
|
3816
4505
|
_greaterThan,
|
|
3817
4506
|
addWhalyFields,
|
|
3818
4507
|
checkCsvHeaderRow,
|
|
4508
|
+
collectPaginated,
|
|
3819
4509
|
countCsvLines,
|
|
3820
4510
|
createCellReference,
|
|
3821
4511
|
createCsvStreamConfig,
|
|
@@ -3823,6 +4513,7 @@ export {
|
|
|
3823
4513
|
createExcelStreamConfig,
|
|
3824
4514
|
createTemporaryFileStream,
|
|
3825
4515
|
csvFieldsToJsonSchema,
|
|
4516
|
+
downloadFiles,
|
|
3826
4517
|
excelColumnToIndex,
|
|
3827
4518
|
excelFieldsToJsonSchema,
|
|
3828
4519
|
excelRowToIndex,
|
|
@@ -3835,29 +4526,40 @@ export {
|
|
|
3835
4526
|
findAllMatchingConfigs,
|
|
3836
4527
|
flattenSchema,
|
|
3837
4528
|
getAllMetrics,
|
|
4529
|
+
getApiEndpoint,
|
|
3838
4530
|
getAxiosInstance,
|
|
4531
|
+
getCdnId,
|
|
3839
4532
|
getCounterMetric,
|
|
3840
4533
|
getCounterMetrics,
|
|
3841
4534
|
getDownloadFileApiCall,
|
|
4535
|
+
getDryRunLimit,
|
|
3842
4536
|
getJSONApiCall,
|
|
3843
4537
|
getJSONApiCallWithFullResponse,
|
|
4538
|
+
getMimeType,
|
|
3844
4539
|
getRawJSONApiCall,
|
|
4540
|
+
getServiceAccountKey,
|
|
3845
4541
|
gracefulExit,
|
|
3846
4542
|
haltAndCatchFire,
|
|
3847
4543
|
incrementStreamState,
|
|
3848
4544
|
indexToExcelColumn,
|
|
3849
4545
|
indexToExcelRow,
|
|
4546
|
+
isDryRun,
|
|
4547
|
+
listFilesRecursively,
|
|
3850
4548
|
loadJson,
|
|
3851
4549
|
loadSchema,
|
|
3852
4550
|
logger,
|
|
4551
|
+
optionalEnv,
|
|
3853
4552
|
parseCellReference,
|
|
3854
4553
|
postFormDataApiCall,
|
|
3855
4554
|
postJSONApiCall,
|
|
3856
4555
|
postUrlEncodedApiCall,
|
|
3857
4556
|
printMemoryFootprint,
|
|
3858
4557
|
processFileStreams,
|
|
4558
|
+
processFromAsyncIterable,
|
|
3859
4559
|
removeParasiteProperties,
|
|
4560
|
+
requireEnv,
|
|
3860
4561
|
rowGeneratorFromCsv,
|
|
4562
|
+
runWithConcurrency,
|
|
3861
4563
|
safePath,
|
|
3862
4564
|
unzip,
|
|
3863
4565
|
validateExcelConfig,
|