@whaly/connector-sdk 0.2.0 → 0.3.0
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/README.md +92 -0
- package/dist/index.d.mts +458 -2
- package/dist/index.d.ts +458 -2
- package/dist/index.js +1181 -148
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1147 -147
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -5
package/dist/index.mjs
CHANGED
|
@@ -380,6 +380,7 @@ var haltAndCatchFire = async (errorType, errorText, errorDebugText) => {
|
|
|
380
380
|
var ReplicationMethod = /* @__PURE__ */ ((ReplicationMethod2) => {
|
|
381
381
|
ReplicationMethod2["INCREMENTAL"] = "INCREMENTAL";
|
|
382
382
|
ReplicationMethod2["FULL_TABLE"] = "FULL_TABLE";
|
|
383
|
+
ReplicationMethod2["APPEND"] = "APPEND";
|
|
383
384
|
return ReplicationMethod2;
|
|
384
385
|
})(ReplicationMethod || {});
|
|
385
386
|
|
|
@@ -1504,6 +1505,8 @@ var StreamWarehouseSyncService = class {
|
|
|
1504
1505
|
mergeQueries = this.getReplaceQueries();
|
|
1505
1506
|
} else if (replicationMethod === "INCREMENTAL") {
|
|
1506
1507
|
mergeQueries = this.getMergeQueries();
|
|
1508
|
+
} else if (replicationMethod === "APPEND") {
|
|
1509
|
+
mergeQueries = this.getAppendQueries();
|
|
1507
1510
|
} else if (replicationMethod === "LOG_BASED") {
|
|
1508
1511
|
mergeQueries = this.getMergeQueries();
|
|
1509
1512
|
}
|
|
@@ -1685,15 +1688,15 @@ import fs2 from "fs-extra";
|
|
|
1685
1688
|
import uniqueId from "lodash/uniqueId.js";
|
|
1686
1689
|
var createTemporaryFileStream = (streamId) => {
|
|
1687
1690
|
fs2.ensureDirSync("./tmp");
|
|
1688
|
-
const
|
|
1689
|
-
const writeStream = fs2.createWriteStream(
|
|
1691
|
+
const path2 = `./tmp/${safePath(streamId)}-${uniqueId()}.tmp`;
|
|
1692
|
+
const writeStream = fs2.createWriteStream(path2, { flags: "w" });
|
|
1690
1693
|
return {
|
|
1691
1694
|
stream: writeStream,
|
|
1692
|
-
path
|
|
1695
|
+
path: path2
|
|
1693
1696
|
};
|
|
1694
1697
|
};
|
|
1695
|
-
function safePath(
|
|
1696
|
-
let formattedPath =
|
|
1698
|
+
function safePath(path2) {
|
|
1699
|
+
let formattedPath = path2;
|
|
1697
1700
|
formattedPath = formattedPath.replace(/^The\s+/gi, "");
|
|
1698
1701
|
formattedPath = formattedPath.replace(/\s+(?:\(|\[)?Dis(?:c|k) (?:One|Two|Three|Four|Five|Six|Seven|Eight|Nine|Ten|\d)+(?:\)|\])?/gi, "");
|
|
1699
1702
|
formattedPath = formattedPath.replace(/\s+/gi, "_");
|
|
@@ -2002,7 +2005,8 @@ var ITarget = class _ITarget {
|
|
|
2002
2005
|
}
|
|
2003
2006
|
});
|
|
2004
2007
|
streamState.setSchema(schema);
|
|
2005
|
-
|
|
2008
|
+
const streamReplicationMethod = this.streams[streamId]?.getReplicationMethod();
|
|
2009
|
+
if (message.keyProperties.length === 0 && streamReplicationMethod !== "APPEND" /* APPEND */) {
|
|
2006
2010
|
throw new Error(`StreamId: ${message.stream} - \`key_properties\` field is required and can't be empty in SCHEMA message.
|
|
2007
2011
|
Received SCHEMA message: ${JSON.stringify(message)}`);
|
|
2008
2012
|
}
|
|
@@ -2066,7 +2070,8 @@ var ITarget = class _ITarget {
|
|
|
2066
2070
|
return false;
|
|
2067
2071
|
}
|
|
2068
2072
|
const batchedRecords = stream.getBatchedRowCount();
|
|
2069
|
-
|
|
2073
|
+
const method = stream.getReplicationMethod();
|
|
2074
|
+
if ((method === "INCREMENTAL" || method === "APPEND") && batchedRecords > 1e6) {
|
|
2070
2075
|
logger.info(`\u{1F9CA} Stream=${streamId} has reached the maximum amount of batched records. We'll trigger a flush of ALL streams in the Warehouse.`);
|
|
2071
2076
|
return true;
|
|
2072
2077
|
} else {
|
|
@@ -2135,6 +2140,1071 @@ var ITarget = class _ITarget {
|
|
|
2135
2140
|
};
|
|
2136
2141
|
};
|
|
2137
2142
|
|
|
2143
|
+
// src/sdk/file-processing/types.ts
|
|
2144
|
+
var FileFormat = /* @__PURE__ */ ((FileFormat2) => {
|
|
2145
|
+
FileFormat2["CSV"] = "CSV";
|
|
2146
|
+
FileFormat2["EXCEL"] = "EXCEL";
|
|
2147
|
+
return FileFormat2;
|
|
2148
|
+
})(FileFormat || {});
|
|
2149
|
+
|
|
2150
|
+
// src/sdk/file-processing/schema-utils.ts
|
|
2151
|
+
function fieldTypeToJsonSchema(fieldType) {
|
|
2152
|
+
switch (fieldType) {
|
|
2153
|
+
case "STRING":
|
|
2154
|
+
return { type: ["null", "string"] };
|
|
2155
|
+
case "FLOAT":
|
|
2156
|
+
return { type: ["null", "number"] };
|
|
2157
|
+
case "TIMESTAMP":
|
|
2158
|
+
return { type: ["null", "string"], format: "date-time" };
|
|
2159
|
+
default:
|
|
2160
|
+
return { type: ["null", "string"] };
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
function excelFieldsToJsonSchema(fields) {
|
|
2164
|
+
const properties = {};
|
|
2165
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
2166
|
+
properties[key] = fieldTypeToJsonSchema(field.type);
|
|
2167
|
+
}
|
|
2168
|
+
return { type: "object", properties };
|
|
2169
|
+
}
|
|
2170
|
+
function csvFieldsToJsonSchema(config) {
|
|
2171
|
+
const properties = {};
|
|
2172
|
+
if (Array.isArray(config)) {
|
|
2173
|
+
for (const field of config) {
|
|
2174
|
+
properties[field.key] = fieldTypeToJsonSchema(field.type);
|
|
2175
|
+
}
|
|
2176
|
+
} else {
|
|
2177
|
+
for (const [, field] of Object.entries(config)) {
|
|
2178
|
+
properties[field.key] = fieldTypeToJsonSchema(field.type);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
return { type: "object", properties };
|
|
2182
|
+
}
|
|
2183
|
+
function extractPrimaryKeysFromCsvConfig(config) {
|
|
2184
|
+
if (Array.isArray(config)) {
|
|
2185
|
+
return config.map((f) => f.key);
|
|
2186
|
+
}
|
|
2187
|
+
return Object.values(config).map((f) => f.key);
|
|
2188
|
+
}
|
|
2189
|
+
function extractPrimaryKeysFromExcelFields(fields) {
|
|
2190
|
+
return Object.entries(fields).filter(([, field]) => {
|
|
2191
|
+
return field.primaryKey === true || field.primaryKey === true;
|
|
2192
|
+
}).map(([key]) => key);
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
// src/sdk/file-processing/excel/reader.ts
|
|
2196
|
+
import XLSX from "xlsx";
|
|
2197
|
+
import path from "path";
|
|
2198
|
+
var excelColumnToIndex = (columnLetter) => {
|
|
2199
|
+
const letters = columnLetter.toUpperCase();
|
|
2200
|
+
let index = 0;
|
|
2201
|
+
for (let i = 0; i < letters.length; i++) {
|
|
2202
|
+
const charCode = letters.charCodeAt(i);
|
|
2203
|
+
if (charCode < 65 || charCode > 90) {
|
|
2204
|
+
throw new Error(`Invalid column letter: ${columnLetter}`);
|
|
2205
|
+
}
|
|
2206
|
+
index = index * 26 + (charCode - 64);
|
|
2207
|
+
}
|
|
2208
|
+
return index - 1;
|
|
2209
|
+
};
|
|
2210
|
+
var indexToExcelColumn = (columnIndex) => {
|
|
2211
|
+
if (columnIndex < 0) {
|
|
2212
|
+
throw new Error(`Invalid column index: ${columnIndex}. Must be >= 0`);
|
|
2213
|
+
}
|
|
2214
|
+
let result = "";
|
|
2215
|
+
let index = columnIndex;
|
|
2216
|
+
while (true) {
|
|
2217
|
+
result = String.fromCharCode(65 + index % 26) + result;
|
|
2218
|
+
index = Math.floor(index / 26);
|
|
2219
|
+
if (index === 0) break;
|
|
2220
|
+
index--;
|
|
2221
|
+
}
|
|
2222
|
+
return result;
|
|
2223
|
+
};
|
|
2224
|
+
var excelRowToIndex = (rowNumber) => {
|
|
2225
|
+
const rowNum = typeof rowNumber === "string" ? parseInt(rowNumber, 10) : rowNumber;
|
|
2226
|
+
if (isNaN(rowNum) || rowNum < 1) {
|
|
2227
|
+
throw new Error(`Invalid row number: ${rowNumber}. Must be >= 1`);
|
|
2228
|
+
}
|
|
2229
|
+
return rowNum - 1;
|
|
2230
|
+
};
|
|
2231
|
+
var indexToExcelRow = (index) => {
|
|
2232
|
+
if (index < 0) {
|
|
2233
|
+
throw new Error(`Invalid index: ${index}. Must be >= 0`);
|
|
2234
|
+
}
|
|
2235
|
+
return index + 1;
|
|
2236
|
+
};
|
|
2237
|
+
var parseCellReference = (cellRef) => {
|
|
2238
|
+
const match = cellRef.match(/^([A-Z]+)(\d+)$/);
|
|
2239
|
+
if (!match) {
|
|
2240
|
+
throw new Error(`Invalid cell reference: ${cellRef}. Expected format like "A1", "Z24", "AA100"`);
|
|
2241
|
+
}
|
|
2242
|
+
const [, columnLetter, rowNumber] = match;
|
|
2243
|
+
return {
|
|
2244
|
+
columnIndex: excelColumnToIndex(columnLetter),
|
|
2245
|
+
rowIndex: excelRowToIndex(rowNumber)
|
|
2246
|
+
};
|
|
2247
|
+
};
|
|
2248
|
+
var createCellReference = (columnIndex, rowIndex) => {
|
|
2249
|
+
return indexToExcelColumn(columnIndex) + indexToExcelRow(rowIndex);
|
|
2250
|
+
};
|
|
2251
|
+
var extractSingleSheetRows = async (fileName, conf) => {
|
|
2252
|
+
console.log("Extracting single sheet rows from file: %s", fileName);
|
|
2253
|
+
const workbook = XLSX.readFile(fileName, { dense: true });
|
|
2254
|
+
const sheetKeys = Object.keys(workbook.Sheets);
|
|
2255
|
+
const sheet = conf.sheetName ? workbook.Sheets[conf.sheetName] : workbook.Sheets[sheetKeys[0]];
|
|
2256
|
+
if (!sheet) {
|
|
2257
|
+
throw new Error(`Sheet ${conf.sheetName} not found in workbook`);
|
|
2258
|
+
}
|
|
2259
|
+
const sheetData = sheet["!data"];
|
|
2260
|
+
if (!sheetData) {
|
|
2261
|
+
throw new Error(`No data in ${conf.sheetName || "first"} sheet`);
|
|
2262
|
+
}
|
|
2263
|
+
if (sheetData.length === 0) {
|
|
2264
|
+
throw new Error(`Sheet ${conf.sheetName || "first"} is empty`);
|
|
2265
|
+
}
|
|
2266
|
+
const variables = conf.fileNameVariablesExtractor(fileName, workbook);
|
|
2267
|
+
const data = [];
|
|
2268
|
+
let rowIdx = 0;
|
|
2269
|
+
const minColumnIndex = Object.values(conf.columns).reduce((min, column) => {
|
|
2270
|
+
return column.column ? Math.min(min, excelColumnToIndex(column.column)) : min;
|
|
2271
|
+
}, Number.MAX_SAFE_INTEGER);
|
|
2272
|
+
for (const row of sheetData) {
|
|
2273
|
+
if (rowIdx < conf.numberOfRowsToSkip) {
|
|
2274
|
+
rowIdx++;
|
|
2275
|
+
continue;
|
|
2276
|
+
}
|
|
2277
|
+
if (!row) {
|
|
2278
|
+
console.log("Empty row at index: %s, skipped processing.", rowIdx);
|
|
2279
|
+
continue;
|
|
2280
|
+
}
|
|
2281
|
+
if (minColumnIndex !== Number.MAX_SAFE_INTEGER && !row[minColumnIndex]?.v) {
|
|
2282
|
+
console.log("stopping processing because of empty value in the first data column (idx: %s)", minColumnIndex);
|
|
2283
|
+
break;
|
|
2284
|
+
}
|
|
2285
|
+
const rowData = Object.entries(conf.columns).reduce((acc, [key, column]) => {
|
|
2286
|
+
if (column.variableName) {
|
|
2287
|
+
if (!variables) {
|
|
2288
|
+
throw new Error(`No variables extracted from filename, cannot extract derived field ${key}`);
|
|
2289
|
+
}
|
|
2290
|
+
acc[key] = variables[column.variableName] || "";
|
|
2291
|
+
} else {
|
|
2292
|
+
const colIndex = excelColumnToIndex(column.column);
|
|
2293
|
+
acc[key] = row[colIndex]?.v || "";
|
|
2294
|
+
}
|
|
2295
|
+
return acc;
|
|
2296
|
+
}, {});
|
|
2297
|
+
data.push(rowData);
|
|
2298
|
+
rowIdx++;
|
|
2299
|
+
}
|
|
2300
|
+
console.log("Extracted %s rows from sheet %s", data.length, conf.sheetName || "first");
|
|
2301
|
+
return data;
|
|
2302
|
+
};
|
|
2303
|
+
var extractExcelRows = async (localFilePath, conf) => {
|
|
2304
|
+
let data;
|
|
2305
|
+
if (conf.type === "single-sheet-extraction") {
|
|
2306
|
+
data = await extractSingleSheetRows(localFilePath, conf);
|
|
2307
|
+
} else if (conf.type === "processor") {
|
|
2308
|
+
const workbook = XLSX.readFile(localFilePath, { dense: true });
|
|
2309
|
+
data = await conf.processor(workbook);
|
|
2310
|
+
} else {
|
|
2311
|
+
throw new Error(`Unsupported configuration type: ${conf.type}`);
|
|
2312
|
+
}
|
|
2313
|
+
return data;
|
|
2314
|
+
};
|
|
2315
|
+
var validateExcelConfig = (config) => {
|
|
2316
|
+
if (!config.extension) {
|
|
2317
|
+
throw new Error("Extension is required");
|
|
2318
|
+
}
|
|
2319
|
+
if (!config.tableName) {
|
|
2320
|
+
throw new Error("Table name is required");
|
|
2321
|
+
}
|
|
2322
|
+
if (!config.fileNameValidator || typeof config.fileNameValidator !== "function") {
|
|
2323
|
+
throw new Error("File name validator function is required");
|
|
2324
|
+
}
|
|
2325
|
+
if (!config.fileNameVariablesExtractor || typeof config.fileNameVariablesExtractor !== "function") {
|
|
2326
|
+
throw new Error("File name variables extractor function is required");
|
|
2327
|
+
}
|
|
2328
|
+
if (config.type === "single-sheet-extraction" && (!config.columns || Object.keys(config.columns).length === 0)) {
|
|
2329
|
+
throw new Error("At least one column configuration is required for single-sheet-extraction");
|
|
2330
|
+
}
|
|
2331
|
+
if (config.type === "single-sheet-extraction") {
|
|
2332
|
+
Object.entries(config.columns).forEach(([key, column]) => {
|
|
2333
|
+
if (!column.type) {
|
|
2334
|
+
throw new Error(`Column ${key} must have a type`);
|
|
2335
|
+
}
|
|
2336
|
+
if (column.column) {
|
|
2337
|
+
const colLetter = column.column;
|
|
2338
|
+
if (!/^[A-Z]+$/i.test(colLetter)) {
|
|
2339
|
+
throw new Error(`Invalid column letter format: ${colLetter}`);
|
|
2340
|
+
}
|
|
2341
|
+
} else if (column.variableName) {
|
|
2342
|
+
} else {
|
|
2343
|
+
throw new Error(`Column ${key} must specify either 'column' or 'variableName'`);
|
|
2344
|
+
}
|
|
2345
|
+
});
|
|
2346
|
+
if (config.numberOfRowsToSkip === void 0 || config.numberOfRowsToSkip < 0) {
|
|
2347
|
+
throw new Error("Number of rows to skip must be specified and non-negative");
|
|
2348
|
+
}
|
|
2349
|
+
} else if (config.type === "processor") {
|
|
2350
|
+
if (!config.processor || typeof config.processor !== "function") {
|
|
2351
|
+
throw new Error("Processor function is required for processor type");
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
};
|
|
2355
|
+
var findAllMatchingConfigs = (filename, configs) => {
|
|
2356
|
+
const fileInfo = path.parse(filename);
|
|
2357
|
+
return configs.filter(
|
|
2358
|
+
(config) => config.fileNameValidator(fileInfo.base) && config.extension === fileInfo.ext.replace(".", "")
|
|
2359
|
+
);
|
|
2360
|
+
};
|
|
2361
|
+
async function* createExcelGenerator(data) {
|
|
2362
|
+
for (const row of data) {
|
|
2363
|
+
yield row;
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
// src/sdk/file-processing/excel/config-builder.ts
|
|
2368
|
+
var ExcelExtractionConfigBuilder = class _ExcelExtractionConfigBuilder {
|
|
2369
|
+
config = {
|
|
2370
|
+
fileNameValidator: () => true,
|
|
2371
|
+
fileNameVariablesExtractor: () => ({}),
|
|
2372
|
+
replicationMethod: "FULL_TABLE" /* FULL_TABLE */
|
|
2373
|
+
};
|
|
2374
|
+
static create() {
|
|
2375
|
+
return new _ExcelExtractionConfigBuilder();
|
|
2376
|
+
}
|
|
2377
|
+
extension(ext) {
|
|
2378
|
+
this.config.extension = ext;
|
|
2379
|
+
return this;
|
|
2380
|
+
}
|
|
2381
|
+
tableName(name) {
|
|
2382
|
+
this.config.tableName = name;
|
|
2383
|
+
return this;
|
|
2384
|
+
}
|
|
2385
|
+
fileValidator(validator) {
|
|
2386
|
+
this.config.fileNameValidator = validator;
|
|
2387
|
+
return this;
|
|
2388
|
+
}
|
|
2389
|
+
variablesExtractor(extractor) {
|
|
2390
|
+
this.config.fileNameVariablesExtractor = extractor;
|
|
2391
|
+
return this;
|
|
2392
|
+
}
|
|
2393
|
+
replicationMethod(method) {
|
|
2394
|
+
this.config.replicationMethod = method;
|
|
2395
|
+
return this;
|
|
2396
|
+
}
|
|
2397
|
+
columns(cols) {
|
|
2398
|
+
if (this.config.type === "single-sheet-extraction") {
|
|
2399
|
+
this.config.columns = cols;
|
|
2400
|
+
} else {
|
|
2401
|
+
throw new Error("Columns can only be set for single-sheet-extraction");
|
|
2402
|
+
}
|
|
2403
|
+
return this;
|
|
2404
|
+
}
|
|
2405
|
+
singleSheet(sheetName, skipRows = 0) {
|
|
2406
|
+
this.config.type = "single-sheet-extraction";
|
|
2407
|
+
if (sheetName !== void 0) {
|
|
2408
|
+
this.config.sheetName = sheetName;
|
|
2409
|
+
}
|
|
2410
|
+
this.config.numberOfRowsToSkip = skipRows;
|
|
2411
|
+
return this;
|
|
2412
|
+
}
|
|
2413
|
+
processor(processorFn) {
|
|
2414
|
+
this.config.type = "processor";
|
|
2415
|
+
this.config.processor = processorFn;
|
|
2416
|
+
return this;
|
|
2417
|
+
}
|
|
2418
|
+
build() {
|
|
2419
|
+
if (!this.config.type) {
|
|
2420
|
+
throw new Error("Configuration type must be specified (singleSheet or processor)");
|
|
2421
|
+
}
|
|
2422
|
+
if (!this.config.extension) {
|
|
2423
|
+
throw new Error("Extension must be set");
|
|
2424
|
+
}
|
|
2425
|
+
if (!this.config.tableName) {
|
|
2426
|
+
throw new Error("Table name must be set");
|
|
2427
|
+
}
|
|
2428
|
+
if (!this.config.replicationMethod) {
|
|
2429
|
+
throw new Error("Replication method must be set");
|
|
2430
|
+
}
|
|
2431
|
+
if (typeof this.config.fileNameValidator !== "function") {
|
|
2432
|
+
throw new Error("fileNameValidator must be a function");
|
|
2433
|
+
}
|
|
2434
|
+
if (typeof this.config.fileNameVariablesExtractor !== "function") {
|
|
2435
|
+
throw new Error("fileNameVariablesExtractor must be a function");
|
|
2436
|
+
}
|
|
2437
|
+
if (this.config.type === "single-sheet-extraction") {
|
|
2438
|
+
const singleSheetConfig = this.config;
|
|
2439
|
+
if (!singleSheetConfig.columns || Object.keys(singleSheetConfig.columns).length === 0) {
|
|
2440
|
+
throw new Error("Columns must be set with at least one column for single-sheet-extraction");
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
if (this.config.type === "processor") {
|
|
2444
|
+
const processorConfig = this.config;
|
|
2445
|
+
if (typeof processorConfig.processor !== "function") {
|
|
2446
|
+
throw new Error("Processor function must be set for processor configs");
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
return this.config;
|
|
2450
|
+
}
|
|
2451
|
+
};
|
|
2452
|
+
|
|
2453
|
+
// src/sdk/file-processing/csv/reader.ts
|
|
2454
|
+
import csv from "csv-parser";
|
|
2455
|
+
import { createReadStream } from "fs";
|
|
2456
|
+
import { format as format4 } from "util";
|
|
2457
|
+
import { Readable, Transform } from "stream";
|
|
2458
|
+
import * as iconv from "iconv-lite";
|
|
2459
|
+
var logPrefix = "[csvReader]";
|
|
2460
|
+
var createStripBomTransform = () => {
|
|
2461
|
+
let isFirstChunk = true;
|
|
2462
|
+
return new Transform({
|
|
2463
|
+
transform(chunk2, encoding, callback) {
|
|
2464
|
+
if (isFirstChunk) {
|
|
2465
|
+
isFirstChunk = false;
|
|
2466
|
+
if (chunk2.length >= 3 && chunk2[0] === 239 && chunk2[1] === 187 && chunk2[2] === 191) {
|
|
2467
|
+
chunk2 = chunk2.slice(3);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
callback(null, chunk2);
|
|
2471
|
+
}
|
|
2472
|
+
});
|
|
2473
|
+
};
|
|
2474
|
+
var getStringHexInfo = (str) => {
|
|
2475
|
+
const bytes = Buffer.from(str, "utf8");
|
|
2476
|
+
const hex = bytes.toString("hex").match(/.{2}/g)?.join(" ") || "";
|
|
2477
|
+
return {
|
|
2478
|
+
original: str,
|
|
2479
|
+
length: str.length,
|
|
2480
|
+
hex,
|
|
2481
|
+
trimmed: str.trim()
|
|
2482
|
+
};
|
|
2483
|
+
};
|
|
2484
|
+
var formatHexComparison = (expected, actual) => {
|
|
2485
|
+
const expectedInfo = getStringHexInfo(expected);
|
|
2486
|
+
const actualInfo = getStringHexInfo(actual);
|
|
2487
|
+
return `
|
|
2488
|
+
Expected: "${expected}" (${expectedInfo.length} chars) [${expectedInfo.hex}]
|
|
2489
|
+
Actual: "${actual}" (${actualInfo.length} chars) [${actualInfo.hex}]`;
|
|
2490
|
+
};
|
|
2491
|
+
async function* rowGeneratorFromCsv(path2, fileConfig) {
|
|
2492
|
+
const isGeneratorConfigArray = Array.isArray(fileConfig.fields);
|
|
2493
|
+
const csvOptions = { separator: fileConfig.separator };
|
|
2494
|
+
if (isGeneratorConfigArray) {
|
|
2495
|
+
csvOptions.headers = false;
|
|
2496
|
+
}
|
|
2497
|
+
const csvStream = csv(csvOptions);
|
|
2498
|
+
const syncedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2499
|
+
let initialReadStream;
|
|
2500
|
+
let finalInputStream;
|
|
2501
|
+
const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
|
|
2502
|
+
if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
|
|
2503
|
+
initialReadStream = createReadStream(path2);
|
|
2504
|
+
finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
|
|
2505
|
+
} else {
|
|
2506
|
+
initialReadStream = createReadStream(path2);
|
|
2507
|
+
finalInputStream = initialReadStream.pipe(createStripBomTransform());
|
|
2508
|
+
}
|
|
2509
|
+
finalInputStream.pipe(csvStream);
|
|
2510
|
+
try {
|
|
2511
|
+
for await (const row of Readable.from(csvStream)) {
|
|
2512
|
+
if (!isGeneratorConfigArray) {
|
|
2513
|
+
const rowData = Object.keys(fileConfig.fields).reduce((acc, key) => {
|
|
2514
|
+
const keyGenerator = fileConfig.fields[key.trim()];
|
|
2515
|
+
if (!keyGenerator) {
|
|
2516
|
+
return acc;
|
|
2517
|
+
}
|
|
2518
|
+
return {
|
|
2519
|
+
...acc,
|
|
2520
|
+
[keyGenerator.key]: keyGenerator.valueTransformer ? keyGenerator.valueTransformer(row[key]) : row[key]
|
|
2521
|
+
};
|
|
2522
|
+
}, fileConfig.addSyncedAtColumn ? { _wly_synced_at: syncedAt } : {});
|
|
2523
|
+
yield rowData;
|
|
2524
|
+
} else {
|
|
2525
|
+
const rowData = Object.keys(row).reduce((acc, key, i) => {
|
|
2526
|
+
const keyGenerator = fileConfig.fields[i];
|
|
2527
|
+
if (!keyGenerator) {
|
|
2528
|
+
return acc;
|
|
2529
|
+
}
|
|
2530
|
+
return {
|
|
2531
|
+
...acc,
|
|
2532
|
+
[keyGenerator.key]: keyGenerator.valueTransformer ? keyGenerator.valueTransformer(row[i]) : row[i]
|
|
2533
|
+
};
|
|
2534
|
+
}, fileConfig.addSyncedAtColumn ? { _wly_synced_at: syncedAt } : {});
|
|
2535
|
+
yield rowData;
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
} finally {
|
|
2539
|
+
initialReadStream.destroy();
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
var checkCsvHeaderRow = async (path2, fileConfig) => {
|
|
2543
|
+
return new Promise((resolve, reject) => {
|
|
2544
|
+
const colsWithParsingConfig = Object.keys(fileConfig.fields).map((key) => key.trim());
|
|
2545
|
+
console.debug(`${logPrefix} CSV Expected columns:`, colsWithParsingConfig.map((col) => {
|
|
2546
|
+
const info = getStringHexInfo(col);
|
|
2547
|
+
return `"${info.original}" [${info.hex}]`;
|
|
2548
|
+
}));
|
|
2549
|
+
const isGeneratorConfigArray = Array.isArray(fileConfig.fields);
|
|
2550
|
+
const csvOptions2 = { separator: fileConfig.separator };
|
|
2551
|
+
if (isGeneratorConfigArray) {
|
|
2552
|
+
csvOptions2.headers = false;
|
|
2553
|
+
}
|
|
2554
|
+
const csvStream = csv(csvOptions2);
|
|
2555
|
+
let initialReadStream;
|
|
2556
|
+
let finalInputStream;
|
|
2557
|
+
const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
|
|
2558
|
+
if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
|
|
2559
|
+
initialReadStream = createReadStream(path2);
|
|
2560
|
+
finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
|
|
2561
|
+
} else {
|
|
2562
|
+
initialReadStream = createReadStream(path2);
|
|
2563
|
+
finalInputStream = initialReadStream.pipe(createStripBomTransform());
|
|
2564
|
+
}
|
|
2565
|
+
let isFirstLine = true;
|
|
2566
|
+
finalInputStream.pipe(csvStream).on("data", (row) => {
|
|
2567
|
+
if (isFirstLine) {
|
|
2568
|
+
let headers = [];
|
|
2569
|
+
if (isGeneratorConfigArray) {
|
|
2570
|
+
headers = Object.values(row).filter((r) => !!r);
|
|
2571
|
+
} else {
|
|
2572
|
+
headers = Object.keys(row).filter((r) => !!r);
|
|
2573
|
+
}
|
|
2574
|
+
console.debug(`${logPrefix} CSV Detected headers:`, headers.map((h, idx) => {
|
|
2575
|
+
const info = getStringHexInfo(h);
|
|
2576
|
+
return `[${idx}]: "${info.original}" [${info.hex}]`;
|
|
2577
|
+
}));
|
|
2578
|
+
if (isGeneratorConfigArray) {
|
|
2579
|
+
colsWithParsingConfig.forEach((col, i) => {
|
|
2580
|
+
if (col !== headers[i]) {
|
|
2581
|
+
const hexComparison = formatHexComparison(col, headers[i] || "<undefined>");
|
|
2582
|
+
console.error(`${logPrefix} CSV Header Mismatch:${hexComparison}`);
|
|
2583
|
+
throw new Error(format4(
|
|
2584
|
+
`CSV header mismatch at position %s: expected '%s' but got '%s' in file %s`,
|
|
2585
|
+
i,
|
|
2586
|
+
col,
|
|
2587
|
+
headers[i] || "<undefined>",
|
|
2588
|
+
path2
|
|
2589
|
+
));
|
|
2590
|
+
}
|
|
2591
|
+
});
|
|
2592
|
+
} else {
|
|
2593
|
+
colsWithParsingConfig.forEach((col) => {
|
|
2594
|
+
if (!headers.includes(col)) {
|
|
2595
|
+
const colInfo = getStringHexInfo(col);
|
|
2596
|
+
console.error(`CSV Missing column: "${col}" [${colInfo.hex}]`);
|
|
2597
|
+
console.error(`Available headers:`, headers.map((h) => `"${h}" [${getStringHexInfo(h).hex}]`));
|
|
2598
|
+
const trimmedMatch = headers.find((h) => h.trim() === col.trim());
|
|
2599
|
+
if (trimmedMatch) {
|
|
2600
|
+
console.error(`Potential trimmed match: "${trimmedMatch}" [${getStringHexInfo(trimmedMatch).hex}]`);
|
|
2601
|
+
}
|
|
2602
|
+
throw new Error(format4(
|
|
2603
|
+
`CSV missing expected column '%s' in file: %s`,
|
|
2604
|
+
col,
|
|
2605
|
+
path2
|
|
2606
|
+
));
|
|
2607
|
+
}
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
isFirstLine = false;
|
|
2611
|
+
} else {
|
|
2612
|
+
initialReadStream.destroy();
|
|
2613
|
+
}
|
|
2614
|
+
}).on("error", (error) => {
|
|
2615
|
+
reject(new Error(`Error processing CSV file: ${error.message}`));
|
|
2616
|
+
});
|
|
2617
|
+
initialReadStream.on("close", () => {
|
|
2618
|
+
resolve();
|
|
2619
|
+
});
|
|
2620
|
+
});
|
|
2621
|
+
};
|
|
2622
|
+
|
|
2623
|
+
// src/sdk/file-processing/csv/writer.ts
|
|
2624
|
+
import { createObjectCsvWriter } from "csv-writer";
|
|
2625
|
+
import { format as format5 } from "util";
|
|
2626
|
+
var logPrefix2 = "[csvWriter]";
|
|
2627
|
+
var writeDataToCsv = async (fileName, data) => {
|
|
2628
|
+
try {
|
|
2629
|
+
const firstRow = data[0];
|
|
2630
|
+
if (!firstRow) {
|
|
2631
|
+
console.log(`${logPrefix2} No data to write to CSV file=%s`, fileName);
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
const csvWriter = createObjectCsvWriter({
|
|
2635
|
+
path: fileName,
|
|
2636
|
+
header: Object.keys(firstRow).map((col) => {
|
|
2637
|
+
return { id: col, title: col };
|
|
2638
|
+
})
|
|
2639
|
+
});
|
|
2640
|
+
await csvWriter.writeRecords(data);
|
|
2641
|
+
console.log(`${logPrefix2} CSV file=%s written successfully`, fileName);
|
|
2642
|
+
} catch (err) {
|
|
2643
|
+
if (err instanceof Error) {
|
|
2644
|
+
throw new Error(format5(`error while writing csv file=%s, err: %s`, fileName, err.message));
|
|
2645
|
+
}
|
|
2646
|
+
throw err;
|
|
2647
|
+
}
|
|
2648
|
+
};
|
|
2649
|
+
var writeGeneratorToCSV = async (generator, outputFileName) => {
|
|
2650
|
+
try {
|
|
2651
|
+
let rowCount = 0;
|
|
2652
|
+
const firstDataRow = (await generator.next()).value;
|
|
2653
|
+
if (!firstDataRow) {
|
|
2654
|
+
console.log(`${logPrefix2} [%s] first data row of csv empty, skipping`, outputFileName);
|
|
2655
|
+
return 0;
|
|
2656
|
+
}
|
|
2657
|
+
console.log(`${logPrefix2} [%s] inferring headers from first data row`, outputFileName);
|
|
2658
|
+
const headers = Object.keys(firstDataRow).map((col) => {
|
|
2659
|
+
return {
|
|
2660
|
+
id: col,
|
|
2661
|
+
title: col
|
|
2662
|
+
};
|
|
2663
|
+
});
|
|
2664
|
+
const csvWriter = createObjectCsvWriter({
|
|
2665
|
+
path: outputFileName,
|
|
2666
|
+
header: headers,
|
|
2667
|
+
alwaysQuote: true
|
|
2668
|
+
});
|
|
2669
|
+
console.log(`${logPrefix2} [%s] writing csv`, outputFileName);
|
|
2670
|
+
let batch = [firstDataRow];
|
|
2671
|
+
for await (const data of generator) {
|
|
2672
|
+
batch.push(data);
|
|
2673
|
+
rowCount++;
|
|
2674
|
+
if (batch.length > 1e4) {
|
|
2675
|
+
try {
|
|
2676
|
+
await csvWriter.writeRecords(batch);
|
|
2677
|
+
} catch (err) {
|
|
2678
|
+
if (err instanceof Error) {
|
|
2679
|
+
throw new Error(
|
|
2680
|
+
format5(
|
|
2681
|
+
`error while writing batch to csv batch (first 3 records)=%j, err: %s`,
|
|
2682
|
+
batch.slice(0, 3),
|
|
2683
|
+
err.message
|
|
2684
|
+
)
|
|
2685
|
+
);
|
|
2686
|
+
}
|
|
2687
|
+
throw err;
|
|
2688
|
+
}
|
|
2689
|
+
batch = [];
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
await csvWriter.writeRecords(batch);
|
|
2693
|
+
console.log(`${logPrefix2} [%s] csv written successfully with %d rows`, outputFileName, rowCount);
|
|
2694
|
+
return rowCount;
|
|
2695
|
+
} catch (err) {
|
|
2696
|
+
if (err instanceof Error) {
|
|
2697
|
+
throw new Error(format5(`error while writing csv file=%s, err: %s`, outputFileName, err.message));
|
|
2698
|
+
}
|
|
2699
|
+
throw err;
|
|
2700
|
+
}
|
|
2701
|
+
};
|
|
2702
|
+
|
|
2703
|
+
// src/sdk/file-processing/file-stream.ts
|
|
2704
|
+
import XLSX2 from "xlsx";
|
|
2705
|
+
var FileStream = class extends Stream {
|
|
2706
|
+
localFilePaths;
|
|
2707
|
+
constructor(config, localFilePath, tapState, target) {
|
|
2708
|
+
super(config, tapState, target);
|
|
2709
|
+
this.localFilePaths = Array.isArray(localFilePath) ? localFilePath : [localFilePath];
|
|
2710
|
+
this.streamId = config.streamId;
|
|
2711
|
+
this.primaryKey = config.primaryKeys;
|
|
2712
|
+
this.replicationMethod = config.replicationMethod;
|
|
2713
|
+
}
|
|
2714
|
+
/**
|
|
2715
|
+
* Build JSON Schema from the file config's field definitions.
|
|
2716
|
+
*/
|
|
2717
|
+
async getSchema() {
|
|
2718
|
+
if (this.config.format === "EXCEL" /* EXCEL */) {
|
|
2719
|
+
const excelConfig = this.config.excel;
|
|
2720
|
+
if (excelConfig.type === "single-sheet-extraction") {
|
|
2721
|
+
const jsonSchema = excelFieldsToJsonSchema(excelConfig.columns);
|
|
2722
|
+
return { jsonSchema };
|
|
2723
|
+
}
|
|
2724
|
+
return void 0;
|
|
2725
|
+
}
|
|
2726
|
+
if (this.config.format === "CSV" /* CSV */) {
|
|
2727
|
+
const jsonSchema = csvFieldsToJsonSchema(this.config.csv.fields);
|
|
2728
|
+
return { jsonSchema };
|
|
2729
|
+
}
|
|
2730
|
+
throw new Error(`FileStream: No valid format config for stream ${this.streamId}`);
|
|
2731
|
+
}
|
|
2732
|
+
/**
|
|
2733
|
+
* Yield records from all file(s).
|
|
2734
|
+
* When multiple file paths are provided, records are yielded sequentially from each file.
|
|
2735
|
+
*/
|
|
2736
|
+
async *_getRecords() {
|
|
2737
|
+
for (const filePath of this.localFilePaths) {
|
|
2738
|
+
if (this.config.format === "EXCEL" /* EXCEL */) {
|
|
2739
|
+
const data = await extractExcelRows(filePath, this.config.excel);
|
|
2740
|
+
for (const row of data) {
|
|
2741
|
+
yield row;
|
|
2742
|
+
}
|
|
2743
|
+
} else if (this.config.format === "CSV" /* CSV */) {
|
|
2744
|
+
yield* rowGeneratorFromCsv(filePath, this.config.csv);
|
|
2745
|
+
} else {
|
|
2746
|
+
throw new Error(`FileStream: Unsupported format for stream ${this.streamId}`);
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
};
|
|
2751
|
+
function createExcelStreamConfig(excelConfig, fileName, localFilePath) {
|
|
2752
|
+
if (excelConfig.type === "single-sheet-extraction") {
|
|
2753
|
+
const primaryKeys = extractPrimaryKeysFromExcelFields(excelConfig.columns);
|
|
2754
|
+
let tableName2;
|
|
2755
|
+
if (typeof excelConfig.tableName === "function") {
|
|
2756
|
+
const variables = excelConfig.fileNameVariablesExtractor(fileName);
|
|
2757
|
+
tableName2 = excelConfig.tableName(fileName, void 0, variables);
|
|
2758
|
+
} else {
|
|
2759
|
+
tableName2 = excelConfig.tableName;
|
|
2760
|
+
}
|
|
2761
|
+
return {
|
|
2762
|
+
format: "EXCEL" /* EXCEL */,
|
|
2763
|
+
streamId: tableName2,
|
|
2764
|
+
replicationMethod: excelConfig.replicationMethod,
|
|
2765
|
+
primaryKeys,
|
|
2766
|
+
excel: excelConfig
|
|
2767
|
+
};
|
|
2768
|
+
}
|
|
2769
|
+
let tableName;
|
|
2770
|
+
if (typeof excelConfig.tableName === "function") {
|
|
2771
|
+
if (!localFilePath) {
|
|
2772
|
+
throw new Error("localFilePath is required for processor configs with dynamic table names");
|
|
2773
|
+
}
|
|
2774
|
+
const workbook = XLSX2.readFile(localFilePath, { dense: true });
|
|
2775
|
+
const variables = excelConfig.fileNameVariablesExtractor(fileName, workbook);
|
|
2776
|
+
tableName = excelConfig.tableName(fileName, workbook, variables);
|
|
2777
|
+
} else {
|
|
2778
|
+
tableName = excelConfig.tableName;
|
|
2779
|
+
}
|
|
2780
|
+
return {
|
|
2781
|
+
format: "EXCEL" /* EXCEL */,
|
|
2782
|
+
streamId: tableName,
|
|
2783
|
+
replicationMethod: excelConfig.replicationMethod,
|
|
2784
|
+
primaryKeys: [],
|
|
2785
|
+
excel: excelConfig
|
|
2786
|
+
};
|
|
2787
|
+
}
|
|
2788
|
+
function createCsvStreamConfig(streamId, csvConfig, options) {
|
|
2789
|
+
return {
|
|
2790
|
+
format: "CSV" /* CSV */,
|
|
2791
|
+
streamId,
|
|
2792
|
+
replicationMethod: options?.replicationMethod ?? "FULL_TABLE" /* FULL_TABLE */,
|
|
2793
|
+
primaryKeys: options?.primaryKeys ?? extractPrimaryKeysFromCsvConfig(csvConfig.fields),
|
|
2794
|
+
csv: csvConfig
|
|
2795
|
+
};
|
|
2796
|
+
}
|
|
2797
|
+
async function processFileStreams(entries, tapState, target) {
|
|
2798
|
+
for (const entry of entries) {
|
|
2799
|
+
const stream = new FileStream(entry.config, entry.filePath, tapState, target);
|
|
2800
|
+
await stream.sync();
|
|
2801
|
+
}
|
|
2802
|
+
await target.complete();
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
// src/sdk/file-processing/csv/config-builder.ts
|
|
2806
|
+
var CsvExtractionConfigBuilder = class _CsvExtractionConfigBuilder {
|
|
2807
|
+
_separator = ",";
|
|
2808
|
+
_encoding;
|
|
2809
|
+
_addSyncedAtColumn;
|
|
2810
|
+
_fields;
|
|
2811
|
+
_fieldsMode;
|
|
2812
|
+
_replicationMethod;
|
|
2813
|
+
_primaryKeys;
|
|
2814
|
+
static create() {
|
|
2815
|
+
return new _CsvExtractionConfigBuilder();
|
|
2816
|
+
}
|
|
2817
|
+
separator(sep) {
|
|
2818
|
+
this._separator = sep;
|
|
2819
|
+
return this;
|
|
2820
|
+
}
|
|
2821
|
+
encoding(enc) {
|
|
2822
|
+
this._encoding = enc;
|
|
2823
|
+
return this;
|
|
2824
|
+
}
|
|
2825
|
+
addSyncedAtColumn(enabled = true) {
|
|
2826
|
+
this._addSyncedAtColumn = enabled;
|
|
2827
|
+
return this;
|
|
2828
|
+
}
|
|
2829
|
+
fieldsFromDict(mapping) {
|
|
2830
|
+
if (this._fieldsMode === "array") {
|
|
2831
|
+
throw new Error("Cannot use fieldsFromDict after fieldsFromArray");
|
|
2832
|
+
}
|
|
2833
|
+
this._fieldsMode = "dict";
|
|
2834
|
+
this._fields = mapping;
|
|
2835
|
+
return this;
|
|
2836
|
+
}
|
|
2837
|
+
fieldsFromArray(fields) {
|
|
2838
|
+
if (this._fieldsMode === "dict") {
|
|
2839
|
+
throw new Error("Cannot use fieldsFromArray after fieldsFromDict");
|
|
2840
|
+
}
|
|
2841
|
+
this._fieldsMode = "array";
|
|
2842
|
+
this._fields = fields;
|
|
2843
|
+
return this;
|
|
2844
|
+
}
|
|
2845
|
+
replicationMethod(method) {
|
|
2846
|
+
this._replicationMethod = method;
|
|
2847
|
+
return this;
|
|
2848
|
+
}
|
|
2849
|
+
primaryKeys(keys) {
|
|
2850
|
+
this._primaryKeys = keys;
|
|
2851
|
+
return this;
|
|
2852
|
+
}
|
|
2853
|
+
build() {
|
|
2854
|
+
if (!this._fields) {
|
|
2855
|
+
throw new Error("Fields must be configured (use fieldsFromDict or fieldsFromArray)");
|
|
2856
|
+
}
|
|
2857
|
+
if (Array.isArray(this._fields) && this._fields.length === 0) {
|
|
2858
|
+
throw new Error("Fields must not be empty");
|
|
2859
|
+
}
|
|
2860
|
+
if (!Array.isArray(this._fields) && Object.keys(this._fields).length === 0) {
|
|
2861
|
+
throw new Error("Fields must not be empty");
|
|
2862
|
+
}
|
|
2863
|
+
const config = {
|
|
2864
|
+
separator: this._separator,
|
|
2865
|
+
fields: this._fields
|
|
2866
|
+
};
|
|
2867
|
+
if (this._encoding !== void 0) {
|
|
2868
|
+
config.encoding = this._encoding;
|
|
2869
|
+
}
|
|
2870
|
+
if (this._addSyncedAtColumn !== void 0) {
|
|
2871
|
+
config.addSyncedAtColumn = this._addSyncedAtColumn;
|
|
2872
|
+
}
|
|
2873
|
+
return config;
|
|
2874
|
+
}
|
|
2875
|
+
buildStreamConfig(streamId) {
|
|
2876
|
+
const csvConfig = this.build();
|
|
2877
|
+
const options = {};
|
|
2878
|
+
if (this._replicationMethod !== void 0) {
|
|
2879
|
+
options.replicationMethod = this._replicationMethod;
|
|
2880
|
+
}
|
|
2881
|
+
if (this._primaryKeys !== void 0) {
|
|
2882
|
+
options.primaryKeys = this._primaryKeys;
|
|
2883
|
+
}
|
|
2884
|
+
return createCsvStreamConfig(streamId, csvConfig, options);
|
|
2885
|
+
}
|
|
2886
|
+
};
|
|
2887
|
+
|
|
2888
|
+
// src/sdk/file-processing/file-tap.ts
|
|
2889
|
+
var FileTap = class _FileTap extends Tap {
|
|
2890
|
+
entries;
|
|
2891
|
+
constructor(target, config, stateProvider, entries) {
|
|
2892
|
+
super(target, config, stateProvider);
|
|
2893
|
+
this.entries = entries;
|
|
2894
|
+
}
|
|
2895
|
+
/**
|
|
2896
|
+
* Convenience constructor for the common case where all configs share the same file path.
|
|
2897
|
+
*/
|
|
2898
|
+
static fromConfigs(target, config, stateProvider, fileConfigs, sharedFilePath) {
|
|
2899
|
+
const entries = fileConfigs.map((c) => ({
|
|
2900
|
+
config: c,
|
|
2901
|
+
filePath: sharedFilePath
|
|
2902
|
+
}));
|
|
2903
|
+
return new _FileTap(target, config, stateProvider, entries);
|
|
2904
|
+
}
|
|
2905
|
+
async init() {
|
|
2906
|
+
for (const entry of this.entries) {
|
|
2907
|
+
const stream = new FileStream(
|
|
2908
|
+
entry.config,
|
|
2909
|
+
entry.filePath,
|
|
2910
|
+
this.tapState,
|
|
2911
|
+
this.target
|
|
2912
|
+
);
|
|
2913
|
+
this.streams.push(stream);
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
};
|
|
2917
|
+
|
|
2918
|
+
// src/sdk/file-processing/file-patterns.ts
|
|
2919
|
+
var VariableExtractors = class {
|
|
2920
|
+
/**
|
|
2921
|
+
* Returns the filename as a variable.
|
|
2922
|
+
*/
|
|
2923
|
+
static filename() {
|
|
2924
|
+
return (filename) => ({
|
|
2925
|
+
fileName: filename
|
|
2926
|
+
});
|
|
2927
|
+
}
|
|
2928
|
+
/**
|
|
2929
|
+
* Extracts named capture groups from a regex pattern.
|
|
2930
|
+
*/
|
|
2931
|
+
static regex(pattern) {
|
|
2932
|
+
return (filename) => {
|
|
2933
|
+
const match = filename.match(pattern);
|
|
2934
|
+
return match?.groups ?? {};
|
|
2935
|
+
};
|
|
2936
|
+
}
|
|
2937
|
+
/**
|
|
2938
|
+
* Combines multiple extraction functions.
|
|
2939
|
+
*/
|
|
2940
|
+
static combine(...extractors) {
|
|
2941
|
+
return (filename) => {
|
|
2942
|
+
return extractors.reduce((acc, extractor) => {
|
|
2943
|
+
return { ...acc, ...extractor(filename) };
|
|
2944
|
+
}, {});
|
|
2945
|
+
};
|
|
2946
|
+
}
|
|
2947
|
+
};
|
|
2948
|
+
var FilePatterns = class {
|
|
2949
|
+
/**
|
|
2950
|
+
* Creates a validator for files starting with a specific prefix (case-insensitive).
|
|
2951
|
+
*/
|
|
2952
|
+
static startsWith(prefix) {
|
|
2953
|
+
return (filename) => filename.toLowerCase().startsWith(prefix.toLowerCase());
|
|
2954
|
+
}
|
|
2955
|
+
/**
|
|
2956
|
+
* Creates a validator for files matching a regex pattern.
|
|
2957
|
+
*/
|
|
2958
|
+
static regex(pattern) {
|
|
2959
|
+
return (filename) => pattern.test(filename);
|
|
2960
|
+
}
|
|
2961
|
+
/**
|
|
2962
|
+
* Combines multiple validators with AND logic.
|
|
2963
|
+
*/
|
|
2964
|
+
static and(...validators) {
|
|
2965
|
+
return (filename) => validators.every((validator) => validator(filename));
|
|
2966
|
+
}
|
|
2967
|
+
/**
|
|
2968
|
+
* Combines multiple validators with OR logic.
|
|
2969
|
+
*/
|
|
2970
|
+
static or(...validators) {
|
|
2971
|
+
return (filename) => validators.some((validator) => validator(filename));
|
|
2972
|
+
}
|
|
2973
|
+
};
|
|
2974
|
+
|
|
2975
|
+
// src/services/sftp.ts
|
|
2976
|
+
import Client from "ssh2-sftp-client";
|
|
2977
|
+
import { default as default2 } from "ssh2-sftp-client";
|
|
2978
|
+
var SftpService = new Client();
|
|
2979
|
+
|
|
2980
|
+
// src/services/cloud-storage.ts
|
|
2981
|
+
import { Storage } from "@google-cloud/storage";
|
|
2982
|
+
import { format as format6 } from "util";
|
|
2983
|
+
import * as pathModule from "path";
|
|
2984
|
+
import { existsSync, mkdirSync, unlinkSync as unlinkSync2 } from "fs";
|
|
2985
|
+
|
|
2986
|
+
// node_modules/uuid/dist/esm-node/rng.js
|
|
2987
|
+
import crypto from "crypto";
|
|
2988
|
+
var rnds8Pool = new Uint8Array(256);
|
|
2989
|
+
var poolPtr = rnds8Pool.length;
|
|
2990
|
+
function rng() {
|
|
2991
|
+
if (poolPtr > rnds8Pool.length - 16) {
|
|
2992
|
+
crypto.randomFillSync(rnds8Pool);
|
|
2993
|
+
poolPtr = 0;
|
|
2994
|
+
}
|
|
2995
|
+
return rnds8Pool.slice(poolPtr, poolPtr += 16);
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
// node_modules/uuid/dist/esm-node/regex.js
|
|
2999
|
+
var regex_default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;
|
|
3000
|
+
|
|
3001
|
+
// node_modules/uuid/dist/esm-node/validate.js
|
|
3002
|
+
function validate(uuid) {
|
|
3003
|
+
return typeof uuid === "string" && regex_default.test(uuid);
|
|
3004
|
+
}
|
|
3005
|
+
var validate_default = validate;
|
|
3006
|
+
|
|
3007
|
+
// node_modules/uuid/dist/esm-node/stringify.js
|
|
3008
|
+
var byteToHex = [];
|
|
3009
|
+
for (let i = 0; i < 256; ++i) {
|
|
3010
|
+
byteToHex.push((i + 256).toString(16).substr(1));
|
|
3011
|
+
}
|
|
3012
|
+
function stringify3(arr, offset = 0) {
|
|
3013
|
+
const uuid = (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
|
|
3014
|
+
if (!validate_default(uuid)) {
|
|
3015
|
+
throw TypeError("Stringified UUID is invalid");
|
|
3016
|
+
}
|
|
3017
|
+
return uuid;
|
|
3018
|
+
}
|
|
3019
|
+
var stringify_default = stringify3;
|
|
3020
|
+
|
|
3021
|
+
// node_modules/uuid/dist/esm-node/v4.js
|
|
3022
|
+
function v4(options, buf, offset) {
|
|
3023
|
+
options = options || {};
|
|
3024
|
+
const rnds = options.random || (options.rng || rng)();
|
|
3025
|
+
rnds[6] = rnds[6] & 15 | 64;
|
|
3026
|
+
rnds[8] = rnds[8] & 63 | 128;
|
|
3027
|
+
if (buf) {
|
|
3028
|
+
offset = offset || 0;
|
|
3029
|
+
for (let i = 0; i < 16; ++i) {
|
|
3030
|
+
buf[offset + i] = rnds[i];
|
|
3031
|
+
}
|
|
3032
|
+
return buf;
|
|
3033
|
+
}
|
|
3034
|
+
return stringify_default(rnds);
|
|
3035
|
+
}
|
|
3036
|
+
var v4_default = v4;
|
|
3037
|
+
|
|
3038
|
+
// src/services/cloud-storage.ts
|
|
3039
|
+
var logPrefix3 = "[CloudStorageService]";
|
|
3040
|
+
var tmpDir = "tmp";
|
|
3041
|
+
var CloudStorageService = class {
|
|
3042
|
+
storage;
|
|
3043
|
+
bucket;
|
|
3044
|
+
processedSuffix;
|
|
3045
|
+
supportedExtensions;
|
|
3046
|
+
path;
|
|
3047
|
+
constructor(bucketName, path2, opts) {
|
|
3048
|
+
this.storage = new Storage({ retryOptions: { autoRetry: true, maxRetries: 20 } });
|
|
3049
|
+
this.bucket = this.storage.bucket(bucketName);
|
|
3050
|
+
this.path = path2;
|
|
3051
|
+
this.processedSuffix = opts?.processedSuffix ?? ".processed";
|
|
3052
|
+
this.supportedExtensions = opts?.supportedExtensions ?? [];
|
|
3053
|
+
}
|
|
3054
|
+
/**
|
|
3055
|
+
* Lists all files in the bucket with an optional prefix.
|
|
3056
|
+
*/
|
|
3057
|
+
async listFiles(prefix) {
|
|
3058
|
+
const effectivePrefix = prefix || this.path;
|
|
3059
|
+
const [files] = await this.bucket.getFiles(
|
|
3060
|
+
effectivePrefix ? { prefix: effectivePrefix } : void 0
|
|
3061
|
+
);
|
|
3062
|
+
return files.map((f) => f.name);
|
|
3063
|
+
}
|
|
3064
|
+
/**
|
|
3065
|
+
* Returns files that haven't been marked as processed.
|
|
3066
|
+
* Filters by supported extensions if configured.
|
|
3067
|
+
*/
|
|
3068
|
+
async getUnprocessedFiles() {
|
|
3069
|
+
const allFiles = await this.listFiles();
|
|
3070
|
+
const markerFiles = new Set(
|
|
3071
|
+
allFiles.filter((f) => f.endsWith(this.processedSuffix)).map((f) => f.replace(this.processedSuffix, ""))
|
|
3072
|
+
);
|
|
3073
|
+
return allFiles.filter((f) => {
|
|
3074
|
+
if (f.endsWith(this.processedSuffix)) return false;
|
|
3075
|
+
if (f.endsWith("/")) return false;
|
|
3076
|
+
if (this.supportedExtensions.length > 0) {
|
|
3077
|
+
const ext = pathModule.extname(f).toLowerCase();
|
|
3078
|
+
if (!this.supportedExtensions.includes(ext)) return false;
|
|
3079
|
+
}
|
|
3080
|
+
return !markerFiles.has(f);
|
|
3081
|
+
});
|
|
3082
|
+
}
|
|
3083
|
+
/**
|
|
3084
|
+
* Creates a marker file to indicate a file has been processed.
|
|
3085
|
+
*/
|
|
3086
|
+
async createMarkerFile(fileName) {
|
|
3087
|
+
const markerFileName = `${fileName}${this.processedSuffix}`;
|
|
3088
|
+
try {
|
|
3089
|
+
const file = this.bucket.file(markerFileName);
|
|
3090
|
+
await file.save(format6("Marked file %s as processed", fileName));
|
|
3091
|
+
logger.info(`${logPrefix3} Marker file ${markerFileName} created successfully.`);
|
|
3092
|
+
} catch (err) {
|
|
3093
|
+
if (err instanceof Error) {
|
|
3094
|
+
throw new Error(format6(`error while writing marker file=%s, err:%s`, markerFileName, err.message));
|
|
3095
|
+
}
|
|
3096
|
+
throw err;
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
/**
|
|
3100
|
+
* Downloads a file from GCS to a local tmp directory.
|
|
3101
|
+
* Returns the local path of the downloaded file.
|
|
3102
|
+
*/
|
|
3103
|
+
async downloadFile(filePath, fileName) {
|
|
3104
|
+
const file = this.bucket.file(filePath);
|
|
3105
|
+
const tmpDirPath = pathModule.join(process.cwd(), tmpDir);
|
|
3106
|
+
if (!existsSync(tmpDirPath)) {
|
|
3107
|
+
mkdirSync(tmpDirPath);
|
|
3108
|
+
}
|
|
3109
|
+
const destFilename = pathModule.join(process.cwd(), tmpDir, fileName);
|
|
3110
|
+
try {
|
|
3111
|
+
if (existsSync(destFilename)) {
|
|
3112
|
+
unlinkSync2(destFilename);
|
|
3113
|
+
}
|
|
3114
|
+
await file.download({ destination: destFilename });
|
|
3115
|
+
logger.info(`${logPrefix3} Downloaded ${fileName} to ${destFilename}`);
|
|
3116
|
+
return destFilename;
|
|
3117
|
+
} catch (err) {
|
|
3118
|
+
if (err instanceof Error) {
|
|
3119
|
+
throw new Error(format6(
|
|
3120
|
+
`can't download file=%s from bucket, err:%s`,
|
|
3121
|
+
fileName,
|
|
3122
|
+
err.message
|
|
3123
|
+
));
|
|
3124
|
+
}
|
|
3125
|
+
throw err;
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
/**
|
|
3129
|
+
* Uploads a local file to GCS.
|
|
3130
|
+
*/
|
|
3131
|
+
async uploadFile(localPath, destPath) {
|
|
3132
|
+
try {
|
|
3133
|
+
logger.info(`${logPrefix3} preparing to upload '%s' to '%s'`, localPath, destPath);
|
|
3134
|
+
const [file] = await this.bucket.upload(localPath, { destination: destPath });
|
|
3135
|
+
logger.info(`${logPrefix3} file %s has been successfully uploaded into %s`, localPath, destPath);
|
|
3136
|
+
return file;
|
|
3137
|
+
} catch (err) {
|
|
3138
|
+
if (err instanceof Error) {
|
|
3139
|
+
throw new Error(format6(`error while uploading file file=%s into path=%s, err:%s`, localPath, destPath, err.message));
|
|
3140
|
+
}
|
|
3141
|
+
throw err;
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
/**
|
|
3145
|
+
* Reads a GCS object and returns its contents as a UTF-8 string.
|
|
3146
|
+
*/
|
|
3147
|
+
async readObjectAsString(objectPath) {
|
|
3148
|
+
try {
|
|
3149
|
+
await this.bucket.get({ autoCreate: false });
|
|
3150
|
+
const fileRef = this.bucket.file(objectPath);
|
|
3151
|
+
const [exists] = await fileRef.exists();
|
|
3152
|
+
if (!exists) {
|
|
3153
|
+
throw new Error(`GCS object not found: gs://${this.bucket.name}/${objectPath}`);
|
|
3154
|
+
}
|
|
3155
|
+
const [contents] = await fileRef.download();
|
|
3156
|
+
return contents.toString("utf8");
|
|
3157
|
+
} catch (err) {
|
|
3158
|
+
throw new Error(format6(`error reading GCS object gs://${this.bucket.name}/${objectPath}, err: %s`, err?.message));
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
/**
|
|
3162
|
+
* Writes a string to a GCS object with JSON content type.
|
|
3163
|
+
*/
|
|
3164
|
+
async writeStringObject(objectPath, contents) {
|
|
3165
|
+
try {
|
|
3166
|
+
await this.bucket.get({ autoCreate: false });
|
|
3167
|
+
const fileRef = this.bucket.file(objectPath);
|
|
3168
|
+
await fileRef.save(contents, { contentType: "application/json" });
|
|
3169
|
+
logger.info(`Uploaded object to gs://${this.bucket.name}/${objectPath}`);
|
|
3170
|
+
} catch (err) {
|
|
3171
|
+
throw new Error(format6(`error writing GCS object gs://${this.bucket.name}/${objectPath}, err: %s`, err?.message));
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
/**
|
|
3175
|
+
* Uploads a local file to the bucket with a unique name based on prefix, streamId, and UUID.
|
|
3176
|
+
* Returns the GCS File reference.
|
|
3177
|
+
*/
|
|
3178
|
+
async uploadFileWithUniqueName(filePath, prefix, streamId) {
|
|
3179
|
+
try {
|
|
3180
|
+
await this.bucket.get({ autoCreate: false });
|
|
3181
|
+
const destinationFileName = `${prefix}/${streamId}-${v4_default()}.jsonnl`;
|
|
3182
|
+
await this.bucket.upload(filePath, {
|
|
3183
|
+
destination: destinationFileName
|
|
3184
|
+
});
|
|
3185
|
+
logger.info(`Uploaded ${filePath} into ${this.bucket.name}/${destinationFileName} GCS File`);
|
|
3186
|
+
return this.bucket.file(destinationFileName);
|
|
3187
|
+
} catch (err) {
|
|
3188
|
+
logger.error(`Issue when uploading file into GCS bucket for stream: ${streamId}
|
|
3189
|
+
|
|
3190
|
+
Error: ${err.message}
|
|
3191
|
+
Stack: ${err.stack}
|
|
3192
|
+
Code: ${err.code}
|
|
3193
|
+
`);
|
|
3194
|
+
throw err;
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
};
|
|
3198
|
+
|
|
3199
|
+
// src/services/zip.ts
|
|
3200
|
+
import * as fse from "fs-extra";
|
|
3201
|
+
import decompress from "decompress";
|
|
3202
|
+
var unzip = async (zipFilePath, extractedPath) => {
|
|
3203
|
+
await fse.ensureDir(extractedPath);
|
|
3204
|
+
await decompress(zipFilePath, extractedPath);
|
|
3205
|
+
return extractedPath;
|
|
3206
|
+
};
|
|
3207
|
+
|
|
2138
3208
|
// src/targets/bigquery/helpers.ts
|
|
2139
3209
|
var safeColumnName = (key) => {
|
|
2140
3210
|
let returnString = key;
|
|
@@ -2171,8 +3241,8 @@ import Decimal from "decimal.js";
|
|
|
2171
3241
|
import dayjs5 from "dayjs";
|
|
2172
3242
|
var validateDateRange = (record, schema) => {
|
|
2173
3243
|
Object.keys(schema).forEach((key) => {
|
|
2174
|
-
const { format:
|
|
2175
|
-
if (
|
|
3244
|
+
const { format: format8 } = schema[key];
|
|
3245
|
+
if (format8 === "date-time") {
|
|
2176
3246
|
if (record[key]) {
|
|
2177
3247
|
const formattedDate = dayjs5(record[key]).format(defaultDateTimeFormat);
|
|
2178
3248
|
const isNotTooMuchInTheFuture = dayjs5(record[key]).isBefore(dayjs5("9999-12-31 23:59:59.999999"));
|
|
@@ -2256,96 +3326,6 @@ var BqClientHolder = class {
|
|
|
2256
3326
|
}
|
|
2257
3327
|
};
|
|
2258
3328
|
|
|
2259
|
-
// src/targets/bigquery/service/cloudStorage.ts
|
|
2260
|
-
import { Storage } from "@google-cloud/storage";
|
|
2261
|
-
|
|
2262
|
-
// node_modules/uuid/dist/esm-node/rng.js
|
|
2263
|
-
import crypto from "crypto";
|
|
2264
|
-
var rnds8Pool = new Uint8Array(256);
|
|
2265
|
-
var poolPtr = rnds8Pool.length;
|
|
2266
|
-
function rng() {
|
|
2267
|
-
if (poolPtr > rnds8Pool.length - 16) {
|
|
2268
|
-
crypto.randomFillSync(rnds8Pool);
|
|
2269
|
-
poolPtr = 0;
|
|
2270
|
-
}
|
|
2271
|
-
return rnds8Pool.slice(poolPtr, poolPtr += 16);
|
|
2272
|
-
}
|
|
2273
|
-
|
|
2274
|
-
// node_modules/uuid/dist/esm-node/regex.js
|
|
2275
|
-
var regex_default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;
|
|
2276
|
-
|
|
2277
|
-
// node_modules/uuid/dist/esm-node/validate.js
|
|
2278
|
-
function validate(uuid) {
|
|
2279
|
-
return typeof uuid === "string" && regex_default.test(uuid);
|
|
2280
|
-
}
|
|
2281
|
-
var validate_default = validate;
|
|
2282
|
-
|
|
2283
|
-
// node_modules/uuid/dist/esm-node/stringify.js
|
|
2284
|
-
var byteToHex = [];
|
|
2285
|
-
for (let i = 0; i < 256; ++i) {
|
|
2286
|
-
byteToHex.push((i + 256).toString(16).substr(1));
|
|
2287
|
-
}
|
|
2288
|
-
function stringify3(arr, offset = 0) {
|
|
2289
|
-
const uuid = (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
|
|
2290
|
-
if (!validate_default(uuid)) {
|
|
2291
|
-
throw TypeError("Stringified UUID is invalid");
|
|
2292
|
-
}
|
|
2293
|
-
return uuid;
|
|
2294
|
-
}
|
|
2295
|
-
var stringify_default = stringify3;
|
|
2296
|
-
|
|
2297
|
-
// node_modules/uuid/dist/esm-node/v4.js
|
|
2298
|
-
function v4(options, buf, offset) {
|
|
2299
|
-
options = options || {};
|
|
2300
|
-
const rnds = options.random || (options.rng || rng)();
|
|
2301
|
-
rnds[6] = rnds[6] & 15 | 64;
|
|
2302
|
-
rnds[8] = rnds[8] & 63 | 128;
|
|
2303
|
-
if (buf) {
|
|
2304
|
-
offset = offset || 0;
|
|
2305
|
-
for (let i = 0; i < 16; ++i) {
|
|
2306
|
-
buf[offset + i] = rnds[i];
|
|
2307
|
-
}
|
|
2308
|
-
return buf;
|
|
2309
|
-
}
|
|
2310
|
-
return stringify_default(rnds);
|
|
2311
|
-
}
|
|
2312
|
-
var v4_default = v4;
|
|
2313
|
-
|
|
2314
|
-
// src/targets/bigquery/service/cloudStorage.ts
|
|
2315
|
-
var uploadFileInBucket = async (streamId, gcsBuckerName, connectorId, filePath) => {
|
|
2316
|
-
try {
|
|
2317
|
-
const retryOptions = { autoRetry: true, maxRetries: 20 };
|
|
2318
|
-
let storage = new Storage({ retryOptions });
|
|
2319
|
-
const bucketRef = storage.bucket(gcsBuckerName);
|
|
2320
|
-
await bucketRef.get({ autoCreate: false });
|
|
2321
|
-
const destinationFileName = `${connectorId}/${streamId}-${v4_default()}.jsonnl`;
|
|
2322
|
-
await bucketRef.upload(filePath, {
|
|
2323
|
-
destination: destinationFileName
|
|
2324
|
-
});
|
|
2325
|
-
logger.info(`\u{1F5F3} Uploaded ${filePath} into ${gcsBuckerName}/${destinationFileName} GCS File`);
|
|
2326
|
-
return bucketRef.file(destinationFileName);
|
|
2327
|
-
} catch (err) {
|
|
2328
|
-
logger.error(`Issue when uploading file into GCS bucket for stream: ${streamId}
|
|
2329
|
-
|
|
2330
|
-
Error: ${err.message}
|
|
2331
|
-
Stack: ${err.stack}
|
|
2332
|
-
Code: ${err.code}
|
|
2333
|
-
`);
|
|
2334
|
-
if (err.code < 500) {
|
|
2335
|
-
await haltAndCatchFire(
|
|
2336
|
-
`unauthorized`,
|
|
2337
|
-
`We couldn't connect to your Cloud Storage bucket \u{1F614}
|
|
2338
|
-
|
|
2339
|
-
The error from Google is: '${err.message}' \u{1F440}
|
|
2340
|
-
|
|
2341
|
-
Could you troubleshoot your Cloud Storage configuration in Google Cloud and sync again the source? \u{1F64F}`,
|
|
2342
|
-
`Got error from cloud storage lib`
|
|
2343
|
-
);
|
|
2344
|
-
}
|
|
2345
|
-
throw err;
|
|
2346
|
-
}
|
|
2347
|
-
};
|
|
2348
|
-
|
|
2349
3329
|
// src/targets/bigquery/service/dbSync.ts
|
|
2350
3330
|
import chunk from "lodash/chunk.js";
|
|
2351
3331
|
import { Semaphore as Semaphore2 } from "async-mutex";
|
|
@@ -2386,21 +3366,21 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
|
|
|
2386
3366
|
safeColumnName = safeColumnName;
|
|
2387
3367
|
getWarehouseTypeFromJSONSchema = (definition) => {
|
|
2388
3368
|
const type = definition.type;
|
|
2389
|
-
const
|
|
3369
|
+
const format8 = definition.format;
|
|
2390
3370
|
let typeArr;
|
|
2391
3371
|
if (type instanceof Array) {
|
|
2392
3372
|
typeArr = type;
|
|
2393
3373
|
} else {
|
|
2394
3374
|
typeArr = [type];
|
|
2395
3375
|
}
|
|
2396
|
-
if (
|
|
2397
|
-
switch (
|
|
3376
|
+
if (format8) {
|
|
3377
|
+
switch (format8) {
|
|
2398
3378
|
case "date-time":
|
|
2399
3379
|
return "TIMESTAMP";
|
|
2400
3380
|
case "json":
|
|
2401
3381
|
return "STRING";
|
|
2402
3382
|
default:
|
|
2403
|
-
throw new Error(`StreamId: ${this.streamId} - Unsupported format: ${
|
|
3383
|
+
throw new Error(`StreamId: ${this.streamId} - Unsupported format: ${format8}`);
|
|
2404
3384
|
}
|
|
2405
3385
|
} else if (typeArr.includes("number")) {
|
|
2406
3386
|
return "NUMERIC";
|
|
@@ -2496,6 +3476,13 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
|
|
|
2496
3476
|
...this.getMergeQueries()
|
|
2497
3477
|
];
|
|
2498
3478
|
};
|
|
3479
|
+
getAppendQueries = () => {
|
|
3480
|
+
const columnUnsafeToSafeMapping = this.renamedColumnStore.getUnsafeToSafeColumnMapping(this.streamId);
|
|
3481
|
+
const columns = Object.keys(columnUnsafeToSafeMapping).map((unsafeKey) => `\`${columnUnsafeToSafeMapping[unsafeKey]}\``).join(`, `);
|
|
3482
|
+
return [
|
|
3483
|
+
`INSERT INTO ${this.sqlTableId} (${columns}) SELECT ${columns} FROM ${this.sqlStagingTableId};`
|
|
3484
|
+
];
|
|
3485
|
+
};
|
|
2499
3486
|
createDatabaseAndSchemaIfNotExists = async (retryCount) => {
|
|
2500
3487
|
try {
|
|
2501
3488
|
await semaphore2.runExclusive(async () => {
|
|
@@ -2620,11 +3607,11 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
|
|
|
2620
3607
|
};
|
|
2621
3608
|
loadStreamInStagingArea = async (localFilePath) => {
|
|
2622
3609
|
try {
|
|
2623
|
-
const
|
|
2624
|
-
|
|
2625
|
-
|
|
3610
|
+
const gcsService = new CloudStorageService(this.config.loading_deck_gcs_bucket_name);
|
|
3611
|
+
const file = await gcsService.uploadFileWithUniqueName(
|
|
3612
|
+
localFilePath,
|
|
2626
3613
|
this.config.connector_id,
|
|
2627
|
-
|
|
3614
|
+
this.streamId
|
|
2628
3615
|
);
|
|
2629
3616
|
const metadata = {
|
|
2630
3617
|
sourceFormat: "NEWLINE_DELIMITED_JSON",
|
|
@@ -2633,6 +3620,17 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
|
|
|
2633
3620
|
await this.loadGCSFileInTable(this.sqlStagingTableId, file, metadata);
|
|
2634
3621
|
} catch (err) {
|
|
2635
3622
|
logger.error(`StreamId: ${this.streamId} - Error while uploading stream.`);
|
|
3623
|
+
if (err.code < 500) {
|
|
3624
|
+
await haltAndCatchFire(
|
|
3625
|
+
`unauthorized`,
|
|
3626
|
+
`We couldn't connect to your Cloud Storage bucket \u{1F614}
|
|
3627
|
+
|
|
3628
|
+
The error from Google is: '${err.message}' \u{1F440}
|
|
3629
|
+
|
|
3630
|
+
Could you troubleshoot your Cloud Storage configuration in Google Cloud and sync again the source? \u{1F64F}`,
|
|
3631
|
+
`Got error from cloud storage lib`
|
|
3632
|
+
);
|
|
3633
|
+
}
|
|
2636
3634
|
throw err;
|
|
2637
3635
|
}
|
|
2638
3636
|
};
|
|
@@ -2760,47 +3758,16 @@ var BigQueryTarget = class extends ITarget {
|
|
|
2760
3758
|
convertNumberIntoDecimal = convertNumberIntoDecimal;
|
|
2761
3759
|
};
|
|
2762
3760
|
|
|
2763
|
-
// src/sdk/service/gcs.ts
|
|
2764
|
-
import { Storage as Storage2 } from "@google-cloud/storage";
|
|
2765
|
-
import { format as format4 } from "util";
|
|
2766
|
-
var getStorage = () => new Storage2({ retryOptions: { autoRetry: true, maxRetries: 20 } });
|
|
2767
|
-
var readObjectAsString = async (bucketName, objectPath) => {
|
|
2768
|
-
try {
|
|
2769
|
-
const storage = getStorage();
|
|
2770
|
-
const bucketRef = storage.bucket(bucketName);
|
|
2771
|
-
await bucketRef.get({ autoCreate: false });
|
|
2772
|
-
const fileRef = bucketRef.file(objectPath);
|
|
2773
|
-
const [exists] = await fileRef.exists();
|
|
2774
|
-
if (!exists) {
|
|
2775
|
-
throw new Error(`GCS object not found: gs://${bucketName}/${objectPath}`);
|
|
2776
|
-
}
|
|
2777
|
-
const [contents] = await fileRef.download();
|
|
2778
|
-
return contents.toString("utf8");
|
|
2779
|
-
} catch (err) {
|
|
2780
|
-
throw new Error(format4(`error reading GCS object gs://${bucketName}/${objectPath}, err: %s`, err?.message));
|
|
2781
|
-
}
|
|
2782
|
-
};
|
|
2783
|
-
var writeStringObject = async (bucketName, objectPath, contents) => {
|
|
2784
|
-
try {
|
|
2785
|
-
const storage = getStorage();
|
|
2786
|
-
const bucketRef = storage.bucket(bucketName);
|
|
2787
|
-
await bucketRef.get({ autoCreate: false });
|
|
2788
|
-
const fileRef = bucketRef.file(objectPath);
|
|
2789
|
-
await fileRef.save(contents, { contentType: "application/json" });
|
|
2790
|
-
logger.info(`\u{1F5F3} Uploaded object to gs://${bucketName}/${objectPath}`);
|
|
2791
|
-
} catch (err) {
|
|
2792
|
-
throw new Error(format4(`error writing GCS object gs://${bucketName}/${objectPath}, err: %s`, err?.message));
|
|
2793
|
-
}
|
|
2794
|
-
};
|
|
2795
|
-
|
|
2796
3761
|
// src/state-providers/gcs/main.ts
|
|
2797
|
-
import { format as
|
|
3762
|
+
import { format as format7 } from "util";
|
|
2798
3763
|
var GCSStateProvider = class {
|
|
2799
3764
|
bucketName;
|
|
2800
3765
|
connectorId;
|
|
3766
|
+
service;
|
|
2801
3767
|
constructor(connectorId, bucketName) {
|
|
2802
3768
|
this.connectorId = connectorId;
|
|
2803
3769
|
this.bucketName = bucketName;
|
|
3770
|
+
this.service = new CloudStorageService(bucketName);
|
|
2804
3771
|
}
|
|
2805
3772
|
getObjectPath() {
|
|
2806
3773
|
return `${this.connectorId}/state.json`;
|
|
@@ -2809,11 +3776,11 @@ var GCSStateProvider = class {
|
|
|
2809
3776
|
const objectPath = this.getObjectPath();
|
|
2810
3777
|
try {
|
|
2811
3778
|
logger.info(`\u{1F4C1} Loading state from gs://${this.bucketName}/${objectPath}`);
|
|
2812
|
-
const raw = await readObjectAsString(
|
|
3779
|
+
const raw = await this.service.readObjectAsString(objectPath);
|
|
2813
3780
|
const parsed = JSON.parse(raw);
|
|
2814
3781
|
return { state: parsed };
|
|
2815
3782
|
} catch (err) {
|
|
2816
|
-
logger.warn(
|
|
3783
|
+
logger.warn(format7(`error loading state from gs://${this.bucketName}/${objectPath}, will start with empty state, err: %s`, err?.message));
|
|
2817
3784
|
return { state: void 0 };
|
|
2818
3785
|
}
|
|
2819
3786
|
}
|
|
@@ -2821,10 +3788,10 @@ var GCSStateProvider = class {
|
|
|
2821
3788
|
const objectPath = this.getObjectPath();
|
|
2822
3789
|
try {
|
|
2823
3790
|
logger.info(`\u{1F4DD} Writing state to gs://${this.bucketName}/${objectPath}`);
|
|
2824
|
-
await writeStringObject(
|
|
3791
|
+
await this.service.writeStringObject(objectPath, state);
|
|
2825
3792
|
} catch (err) {
|
|
2826
|
-
logger.error(
|
|
2827
|
-
throw new Error(
|
|
3793
|
+
logger.error(format7(`\u{1F4A5} Error while writing state to gs://${this.bucketName}/${objectPath}, err: %s`, err?.message));
|
|
3794
|
+
throw new Error(format7(`error writing state to gs://${this.bucketName}/${objectPath}, err: %s`, err?.message));
|
|
2828
3795
|
}
|
|
2829
3796
|
}
|
|
2830
3797
|
};
|
|
@@ -2834,9 +3801,16 @@ export {
|
|
|
2834
3801
|
Authenticator,
|
|
2835
3802
|
BATCH_INTERVAL_MS,
|
|
2836
3803
|
BigQueryTarget,
|
|
3804
|
+
CloudStorageService,
|
|
2837
3805
|
CounterMetric,
|
|
3806
|
+
CsvExtractionConfigBuilder,
|
|
2838
3807
|
DEFAULT_MAX_CONCURRENT_STREAMS,
|
|
2839
3808
|
EXECUTION_TIME_METRIC_NAME,
|
|
3809
|
+
ExcelExtractionConfigBuilder,
|
|
3810
|
+
FileFormat,
|
|
3811
|
+
FilePatterns,
|
|
3812
|
+
FileStream,
|
|
3813
|
+
FileTap,
|
|
2840
3814
|
GCSStateProvider,
|
|
2841
3815
|
ITarget,
|
|
2842
3816
|
MissingFieldInSchemaError,
|
|
@@ -2849,16 +3823,33 @@ export {
|
|
|
2849
3823
|
ReplicationMethodMessage,
|
|
2850
3824
|
SchemaMessage,
|
|
2851
3825
|
SchemaValidationError,
|
|
3826
|
+
default2 as SftpClient,
|
|
3827
|
+
SftpService,
|
|
2852
3828
|
StateMessage,
|
|
2853
3829
|
StateService,
|
|
2854
3830
|
Stream,
|
|
2855
3831
|
StreamWarehouseSyncService,
|
|
2856
3832
|
Tap,
|
|
3833
|
+
VariableExtractors,
|
|
2857
3834
|
_greaterThan,
|
|
2858
3835
|
addWhalyFields,
|
|
3836
|
+
checkCsvHeaderRow,
|
|
3837
|
+
createCellReference,
|
|
3838
|
+
createCsvStreamConfig,
|
|
3839
|
+
createExcelGenerator,
|
|
3840
|
+
createExcelStreamConfig,
|
|
2859
3841
|
createTemporaryFileStream,
|
|
3842
|
+
csvFieldsToJsonSchema,
|
|
3843
|
+
excelColumnToIndex,
|
|
3844
|
+
excelFieldsToJsonSchema,
|
|
3845
|
+
excelRowToIndex,
|
|
3846
|
+
extractExcelRows,
|
|
3847
|
+
extractPrimaryKeysFromCsvConfig,
|
|
3848
|
+
extractPrimaryKeysFromExcelFields,
|
|
2860
3849
|
extractStateForStream,
|
|
3850
|
+
fieldTypeToJsonSchema,
|
|
2861
3851
|
finalizeStateProgressMarkers,
|
|
3852
|
+
findAllMatchingConfigs,
|
|
2862
3853
|
flattenSchema,
|
|
2863
3854
|
getAllMetrics,
|
|
2864
3855
|
getAxiosInstance,
|
|
@@ -2871,14 +3862,23 @@ export {
|
|
|
2871
3862
|
gracefulExit,
|
|
2872
3863
|
haltAndCatchFire,
|
|
2873
3864
|
incrementStreamState,
|
|
3865
|
+
indexToExcelColumn,
|
|
3866
|
+
indexToExcelRow,
|
|
2874
3867
|
loadJson,
|
|
2875
3868
|
loadSchema,
|
|
2876
3869
|
logger,
|
|
3870
|
+
parseCellReference,
|
|
2877
3871
|
postFormDataApiCall,
|
|
2878
3872
|
postJSONApiCall,
|
|
2879
3873
|
postUrlEncodedApiCall,
|
|
2880
3874
|
printMemoryFootprint,
|
|
3875
|
+
processFileStreams,
|
|
2881
3876
|
removeParasiteProperties,
|
|
2882
|
-
|
|
3877
|
+
rowGeneratorFromCsv,
|
|
3878
|
+
safePath,
|
|
3879
|
+
unzip,
|
|
3880
|
+
validateExcelConfig,
|
|
3881
|
+
writeDataToCsv,
|
|
3882
|
+
writeGeneratorToCSV
|
|
2883
3883
|
};
|
|
2884
3884
|
//# sourceMappingURL=index.mjs.map
|