@whaly/connector-sdk 0.2.0 → 0.3.1
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 +474 -6
- package/dist/index.d.ts +474 -6
- package/dist/index.js +1181 -162
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1146 -161
- package/dist/index.mjs.map +1 -1
- package/package.json +18 -6
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
|
|
|
@@ -727,6 +728,8 @@ var Stream = class {
|
|
|
727
728
|
// However, a STATE will still be emitted if the Stream is using an incremental sync type.
|
|
728
729
|
isSilent = false;
|
|
729
730
|
childConcurrency;
|
|
731
|
+
// Optional total row count for percentage progress logging
|
|
732
|
+
totalRows;
|
|
730
733
|
// Metrics configuration
|
|
731
734
|
rowsSyncedMetricsConf;
|
|
732
735
|
executionTimeMetricsConf;
|
|
@@ -1004,6 +1007,10 @@ var Stream = class {
|
|
|
1004
1007
|
metric.increment();
|
|
1005
1008
|
});
|
|
1006
1009
|
rowsSent += 1;
|
|
1010
|
+
if (rowsSent % 1e3 === 0) {
|
|
1011
|
+
const percentStr = this.totalRows ? ` (${Math.round(rowsSent / this.totalRows * 100)}%)` : "";
|
|
1012
|
+
logger.info(`\u23F3 Stream: ${this.streamId} - ${rowsSent} records synced so far...${percentStr}`);
|
|
1013
|
+
}
|
|
1007
1014
|
}
|
|
1008
1015
|
const stateServiceInst = StateService.getInstance();
|
|
1009
1016
|
const state = stateServiceInst.getBookmark(this.streamId);
|
|
@@ -1504,6 +1511,8 @@ var StreamWarehouseSyncService = class {
|
|
|
1504
1511
|
mergeQueries = this.getReplaceQueries();
|
|
1505
1512
|
} else if (replicationMethod === "INCREMENTAL") {
|
|
1506
1513
|
mergeQueries = this.getMergeQueries();
|
|
1514
|
+
} else if (replicationMethod === "APPEND") {
|
|
1515
|
+
mergeQueries = this.getAppendQueries();
|
|
1507
1516
|
} else if (replicationMethod === "LOG_BASED") {
|
|
1508
1517
|
mergeQueries = this.getMergeQueries();
|
|
1509
1518
|
}
|
|
@@ -1674,7 +1683,8 @@ var flattenSchema = (streamId, schema) => {
|
|
|
1674
1683
|
};
|
|
1675
1684
|
|
|
1676
1685
|
// src/sdk/models/target/target.ts
|
|
1677
|
-
import
|
|
1686
|
+
import Ajv from "ajv";
|
|
1687
|
+
import addFormats from "ajv-formats";
|
|
1678
1688
|
import dayjs4 from "dayjs";
|
|
1679
1689
|
|
|
1680
1690
|
// src/sdk/models/target/streamDbState.ts
|
|
@@ -1685,15 +1695,15 @@ import fs2 from "fs-extra";
|
|
|
1685
1695
|
import uniqueId from "lodash/uniqueId.js";
|
|
1686
1696
|
var createTemporaryFileStream = (streamId) => {
|
|
1687
1697
|
fs2.ensureDirSync("./tmp");
|
|
1688
|
-
const
|
|
1689
|
-
const writeStream = fs2.createWriteStream(
|
|
1698
|
+
const path2 = `./tmp/${safePath(streamId)}-${uniqueId()}.tmp`;
|
|
1699
|
+
const writeStream = fs2.createWriteStream(path2, { flags: "w" });
|
|
1690
1700
|
return {
|
|
1691
1701
|
stream: writeStream,
|
|
1692
|
-
path
|
|
1702
|
+
path: path2
|
|
1693
1703
|
};
|
|
1694
1704
|
};
|
|
1695
|
-
function safePath(
|
|
1696
|
-
let formattedPath =
|
|
1705
|
+
function safePath(path2) {
|
|
1706
|
+
let formattedPath = path2;
|
|
1697
1707
|
formattedPath = formattedPath.replace(/^The\s+/gi, "");
|
|
1698
1708
|
formattedPath = formattedPath.replace(/\s+(?:\(|\[)?Dis(?:c|k) (?:One|Two|Three|Four|Five|Six|Seven|Eight|Nine|Ten|\d)+(?:\)|\])?/gi, "");
|
|
1699
1709
|
formattedPath = formattedPath.replace(/\s+/gi, "_");
|
|
@@ -1752,6 +1762,8 @@ var StreamState = class {
|
|
|
1752
1762
|
// So we need to keep track of whether we are at the first load or not
|
|
1753
1763
|
_hasBeenLoadedYetDuringThisSync;
|
|
1754
1764
|
replicationMethod;
|
|
1765
|
+
// Pre-compiled ajv ValidateFunction — compiled once per schema, reused for every row
|
|
1766
|
+
compiledValidateFn;
|
|
1755
1767
|
constructor(streamId, dbSync, replicationMethod) {
|
|
1756
1768
|
this.streamId = streamId;
|
|
1757
1769
|
this.dbSync = dbSync;
|
|
@@ -1769,6 +1781,12 @@ var StreamState = class {
|
|
|
1769
1781
|
getSchema() {
|
|
1770
1782
|
return this.schema;
|
|
1771
1783
|
}
|
|
1784
|
+
setCompiledValidateFn(fn) {
|
|
1785
|
+
this.compiledValidateFn = fn;
|
|
1786
|
+
}
|
|
1787
|
+
getCompiledValidateFn() {
|
|
1788
|
+
return this.compiledValidateFn;
|
|
1789
|
+
}
|
|
1772
1790
|
getBatchDate() {
|
|
1773
1791
|
return this.batchDate.format(defaultDateTimeFormat);
|
|
1774
1792
|
}
|
|
@@ -1825,8 +1843,8 @@ var ITarget = class _ITarget {
|
|
|
1825
1843
|
flushedState;
|
|
1826
1844
|
renameColumnStore;
|
|
1827
1845
|
streams;
|
|
1828
|
-
// JSON Schema validator
|
|
1829
|
-
|
|
1846
|
+
// JSON Schema validator (ajv — pre-compiles schemas into optimized JS functions)
|
|
1847
|
+
ajv;
|
|
1830
1848
|
constructor(config, stateProvider) {
|
|
1831
1849
|
this.config = config;
|
|
1832
1850
|
this.stateProvider = stateProvider;
|
|
@@ -1834,7 +1852,7 @@ var ITarget = class _ITarget {
|
|
|
1834
1852
|
this.streams = {};
|
|
1835
1853
|
this.batchedState = {};
|
|
1836
1854
|
this.flushedState = {};
|
|
1837
|
-
this.
|
|
1855
|
+
this.ajv = addFormats(new Ajv({ allErrors: true, strict: false }));
|
|
1838
1856
|
this.syncTime = dayjs4();
|
|
1839
1857
|
this.renameColumnStore = new RenameColumnStore();
|
|
1840
1858
|
}
|
|
@@ -1893,12 +1911,14 @@ var ITarget = class _ITarget {
|
|
|
1893
1911
|
if (!stream) {
|
|
1894
1912
|
throw new MissingSchemaError(`A record for stream:\`${streamId}\` was encountered before a corresponding schema.`, streamId);
|
|
1895
1913
|
}
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1914
|
+
if (stream.getBatchedRowCount() % 1e4 === 0) {
|
|
1915
|
+
await semaphore.runExclusive(async () => {
|
|
1916
|
+
const shouldUploadIntermediateBatch = this.shouldUploadIntermediateBatch(streamId);
|
|
1917
|
+
if (shouldUploadIntermediateBatch) {
|
|
1918
|
+
await this.loadAllStreamsInWarehouse({ isFinalLoad: false });
|
|
1919
|
+
}
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1902
1922
|
const record = message.record;
|
|
1903
1923
|
const schema = stream.getSchema();
|
|
1904
1924
|
if (!schema) {
|
|
@@ -1907,13 +1927,13 @@ var ITarget = class _ITarget {
|
|
|
1907
1927
|
}
|
|
1908
1928
|
const recordWithoutParasiteProperties = removeParasiteProperties(record, schema);
|
|
1909
1929
|
const recordWithWhalyFields = addWhalyFields(recordWithoutParasiteProperties, stream.getBatchDate());
|
|
1910
|
-
const
|
|
1911
|
-
if (!
|
|
1930
|
+
const validateFn = stream.getCompiledValidateFn();
|
|
1931
|
+
if (validateFn && !validateFn(recordWithWhalyFields)) {
|
|
1912
1932
|
throw new SchemaValidationError(`Stream: ${streamId} - Record is not valid according to schema.
|
|
1913
|
-
Validation errors: ${JSON.stringify(
|
|
1933
|
+
Validation errors: ${JSON.stringify(validateFn.errors)}
|
|
1914
1934
|
|
|
1915
1935
|
Record: ${JSON.stringify(recordWithoutParasiteProperties)}
|
|
1916
|
-
Schema: ${JSON.stringify(stream.getSchema())}`, streamId,
|
|
1936
|
+
Schema: ${JSON.stringify(stream.getSchema())}`, streamId, validateFn.errors ?? []);
|
|
1917
1937
|
}
|
|
1918
1938
|
const recordWithValidDate = this.validateDateRange(recordWithWhalyFields, schema);
|
|
1919
1939
|
const recordWithDecimal = this.convertNumberIntoDecimal(recordWithValidDate, schema);
|
|
@@ -2002,7 +2022,9 @@ var ITarget = class _ITarget {
|
|
|
2002
2022
|
}
|
|
2003
2023
|
});
|
|
2004
2024
|
streamState.setSchema(schema);
|
|
2005
|
-
|
|
2025
|
+
streamState.setCompiledValidateFn(this.ajv.compile({ type: "object", properties: schema }));
|
|
2026
|
+
const streamReplicationMethod = this.streams[streamId]?.getReplicationMethod();
|
|
2027
|
+
if (message.keyProperties.length === 0 && streamReplicationMethod !== "APPEND" /* APPEND */) {
|
|
2006
2028
|
throw new Error(`StreamId: ${message.stream} - \`key_properties\` field is required and can't be empty in SCHEMA message.
|
|
2007
2029
|
Received SCHEMA message: ${JSON.stringify(message)}`);
|
|
2008
2030
|
}
|
|
@@ -2066,7 +2088,8 @@ var ITarget = class _ITarget {
|
|
|
2066
2088
|
return false;
|
|
2067
2089
|
}
|
|
2068
2090
|
const batchedRecords = stream.getBatchedRowCount();
|
|
2069
|
-
|
|
2091
|
+
const method = stream.getReplicationMethod();
|
|
2092
|
+
if ((method === "INCREMENTAL" || method === "APPEND") && batchedRecords > 1e6) {
|
|
2070
2093
|
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
2094
|
return true;
|
|
2072
2095
|
} else {
|
|
@@ -2135,6 +2158,1037 @@ var ITarget = class _ITarget {
|
|
|
2135
2158
|
};
|
|
2136
2159
|
};
|
|
2137
2160
|
|
|
2161
|
+
// src/sdk/file-processing/types.ts
|
|
2162
|
+
var FileFormat = /* @__PURE__ */ ((FileFormat2) => {
|
|
2163
|
+
FileFormat2["CSV"] = "CSV";
|
|
2164
|
+
FileFormat2["EXCEL"] = "EXCEL";
|
|
2165
|
+
return FileFormat2;
|
|
2166
|
+
})(FileFormat || {});
|
|
2167
|
+
|
|
2168
|
+
// src/sdk/file-processing/schema-utils.ts
|
|
2169
|
+
function fieldTypeToJsonSchema(fieldType) {
|
|
2170
|
+
switch (fieldType) {
|
|
2171
|
+
case "STRING":
|
|
2172
|
+
return { type: ["null", "string"] };
|
|
2173
|
+
case "FLOAT":
|
|
2174
|
+
return { type: ["null", "number"] };
|
|
2175
|
+
case "TIMESTAMP":
|
|
2176
|
+
return { type: ["null", "string"], format: "date-time" };
|
|
2177
|
+
default:
|
|
2178
|
+
return { type: ["null", "string"] };
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
function excelFieldsToJsonSchema(fields) {
|
|
2182
|
+
const properties = {};
|
|
2183
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
2184
|
+
properties[key] = fieldTypeToJsonSchema(field.type);
|
|
2185
|
+
}
|
|
2186
|
+
return { type: "object", properties };
|
|
2187
|
+
}
|
|
2188
|
+
function csvFieldsToJsonSchema(config) {
|
|
2189
|
+
const properties = {};
|
|
2190
|
+
if (Array.isArray(config)) {
|
|
2191
|
+
for (const field of config) {
|
|
2192
|
+
properties[field.key] = fieldTypeToJsonSchema(field.type);
|
|
2193
|
+
}
|
|
2194
|
+
} else {
|
|
2195
|
+
for (const [, field] of Object.entries(config)) {
|
|
2196
|
+
properties[field.key] = fieldTypeToJsonSchema(field.type);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
return { type: "object", properties };
|
|
2200
|
+
}
|
|
2201
|
+
function extractPrimaryKeysFromCsvConfig(config) {
|
|
2202
|
+
if (Array.isArray(config)) {
|
|
2203
|
+
return config.map((f) => f.key);
|
|
2204
|
+
}
|
|
2205
|
+
return Object.values(config).map((f) => f.key);
|
|
2206
|
+
}
|
|
2207
|
+
function extractPrimaryKeysFromExcelFields(fields) {
|
|
2208
|
+
return Object.entries(fields).filter(([, field]) => {
|
|
2209
|
+
return field.primaryKey === true || field.primaryKey === true;
|
|
2210
|
+
}).map(([key]) => key);
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
// src/sdk/file-processing/excel/reader.ts
|
|
2214
|
+
import XLSX from "xlsx";
|
|
2215
|
+
import path from "path";
|
|
2216
|
+
var excelColumnToIndex = (columnLetter) => {
|
|
2217
|
+
const letters = columnLetter.toUpperCase();
|
|
2218
|
+
let index = 0;
|
|
2219
|
+
for (let i = 0; i < letters.length; i++) {
|
|
2220
|
+
const charCode = letters.charCodeAt(i);
|
|
2221
|
+
if (charCode < 65 || charCode > 90) {
|
|
2222
|
+
throw new Error(`Invalid column letter: ${columnLetter}`);
|
|
2223
|
+
}
|
|
2224
|
+
index = index * 26 + (charCode - 64);
|
|
2225
|
+
}
|
|
2226
|
+
return index - 1;
|
|
2227
|
+
};
|
|
2228
|
+
var indexToExcelColumn = (columnIndex) => {
|
|
2229
|
+
if (columnIndex < 0) {
|
|
2230
|
+
throw new Error(`Invalid column index: ${columnIndex}. Must be >= 0`);
|
|
2231
|
+
}
|
|
2232
|
+
let result = "";
|
|
2233
|
+
let index = columnIndex;
|
|
2234
|
+
while (true) {
|
|
2235
|
+
result = String.fromCharCode(65 + index % 26) + result;
|
|
2236
|
+
index = Math.floor(index / 26);
|
|
2237
|
+
if (index === 0) break;
|
|
2238
|
+
index--;
|
|
2239
|
+
}
|
|
2240
|
+
return result;
|
|
2241
|
+
};
|
|
2242
|
+
var excelRowToIndex = (rowNumber) => {
|
|
2243
|
+
const rowNum = typeof rowNumber === "string" ? parseInt(rowNumber, 10) : rowNumber;
|
|
2244
|
+
if (isNaN(rowNum) || rowNum < 1) {
|
|
2245
|
+
throw new Error(`Invalid row number: ${rowNumber}. Must be >= 1`);
|
|
2246
|
+
}
|
|
2247
|
+
return rowNum - 1;
|
|
2248
|
+
};
|
|
2249
|
+
var indexToExcelRow = (index) => {
|
|
2250
|
+
if (index < 0) {
|
|
2251
|
+
throw new Error(`Invalid index: ${index}. Must be >= 0`);
|
|
2252
|
+
}
|
|
2253
|
+
return index + 1;
|
|
2254
|
+
};
|
|
2255
|
+
var parseCellReference = (cellRef) => {
|
|
2256
|
+
const match = cellRef.match(/^([A-Z]+)(\d+)$/);
|
|
2257
|
+
if (!match) {
|
|
2258
|
+
throw new Error(`Invalid cell reference: ${cellRef}. Expected format like "A1", "Z24", "AA100"`);
|
|
2259
|
+
}
|
|
2260
|
+
const [, columnLetter, rowNumber] = match;
|
|
2261
|
+
return {
|
|
2262
|
+
columnIndex: excelColumnToIndex(columnLetter),
|
|
2263
|
+
rowIndex: excelRowToIndex(rowNumber)
|
|
2264
|
+
};
|
|
2265
|
+
};
|
|
2266
|
+
var createCellReference = (columnIndex, rowIndex) => {
|
|
2267
|
+
return indexToExcelColumn(columnIndex) + indexToExcelRow(rowIndex);
|
|
2268
|
+
};
|
|
2269
|
+
var extractSingleSheetRows = async (fileName, conf) => {
|
|
2270
|
+
console.log("Extracting single sheet rows from file: %s", fileName);
|
|
2271
|
+
const workbook = XLSX.readFile(fileName, { dense: true });
|
|
2272
|
+
const sheetKeys = Object.keys(workbook.Sheets);
|
|
2273
|
+
const sheet = conf.sheetName ? workbook.Sheets[conf.sheetName] : workbook.Sheets[sheetKeys[0]];
|
|
2274
|
+
if (!sheet) {
|
|
2275
|
+
throw new Error(`Sheet ${conf.sheetName} not found in workbook`);
|
|
2276
|
+
}
|
|
2277
|
+
const sheetData = sheet["!data"];
|
|
2278
|
+
if (!sheetData) {
|
|
2279
|
+
throw new Error(`No data in ${conf.sheetName || "first"} sheet`);
|
|
2280
|
+
}
|
|
2281
|
+
if (sheetData.length === 0) {
|
|
2282
|
+
throw new Error(`Sheet ${conf.sheetName || "first"} is empty`);
|
|
2283
|
+
}
|
|
2284
|
+
const variables = conf.fileNameVariablesExtractor(fileName, workbook);
|
|
2285
|
+
const data = [];
|
|
2286
|
+
let rowIdx = 0;
|
|
2287
|
+
const minColumnIndex = Object.values(conf.columns).reduce((min, column) => {
|
|
2288
|
+
return column.column ? Math.min(min, excelColumnToIndex(column.column)) : min;
|
|
2289
|
+
}, Number.MAX_SAFE_INTEGER);
|
|
2290
|
+
for (const row of sheetData) {
|
|
2291
|
+
if (rowIdx < conf.numberOfRowsToSkip) {
|
|
2292
|
+
rowIdx++;
|
|
2293
|
+
continue;
|
|
2294
|
+
}
|
|
2295
|
+
if (!row) {
|
|
2296
|
+
console.log("Empty row at index: %s, skipped processing.", rowIdx);
|
|
2297
|
+
continue;
|
|
2298
|
+
}
|
|
2299
|
+
if (minColumnIndex !== Number.MAX_SAFE_INTEGER && !row[minColumnIndex]?.v) {
|
|
2300
|
+
console.log("stopping processing because of empty value in the first data column (idx: %s)", minColumnIndex);
|
|
2301
|
+
break;
|
|
2302
|
+
}
|
|
2303
|
+
const rowData = Object.entries(conf.columns).reduce((acc, [key, column]) => {
|
|
2304
|
+
if (column.variableName) {
|
|
2305
|
+
if (!variables) {
|
|
2306
|
+
throw new Error(`No variables extracted from filename, cannot extract derived field ${key}`);
|
|
2307
|
+
}
|
|
2308
|
+
acc[key] = variables[column.variableName] || "";
|
|
2309
|
+
} else {
|
|
2310
|
+
const colIndex = excelColumnToIndex(column.column);
|
|
2311
|
+
acc[key] = row[colIndex]?.v || "";
|
|
2312
|
+
}
|
|
2313
|
+
return acc;
|
|
2314
|
+
}, {});
|
|
2315
|
+
data.push(rowData);
|
|
2316
|
+
rowIdx++;
|
|
2317
|
+
}
|
|
2318
|
+
console.log("Extracted %s rows from sheet %s", data.length, conf.sheetName || "first");
|
|
2319
|
+
return data;
|
|
2320
|
+
};
|
|
2321
|
+
var extractExcelRows = async (localFilePath, conf) => {
|
|
2322
|
+
let data;
|
|
2323
|
+
if (conf.type === "single-sheet-extraction") {
|
|
2324
|
+
data = await extractSingleSheetRows(localFilePath, conf);
|
|
2325
|
+
} else if (conf.type === "processor") {
|
|
2326
|
+
const workbook = XLSX.readFile(localFilePath, { dense: true });
|
|
2327
|
+
data = await conf.processor(workbook);
|
|
2328
|
+
} else {
|
|
2329
|
+
throw new Error(`Unsupported configuration type: ${conf.type}`);
|
|
2330
|
+
}
|
|
2331
|
+
return data;
|
|
2332
|
+
};
|
|
2333
|
+
var validateExcelConfig = (config) => {
|
|
2334
|
+
if (!config.extension) {
|
|
2335
|
+
throw new Error("Extension is required");
|
|
2336
|
+
}
|
|
2337
|
+
if (!config.tableName) {
|
|
2338
|
+
throw new Error("Table name is required");
|
|
2339
|
+
}
|
|
2340
|
+
if (!config.fileNameValidator || typeof config.fileNameValidator !== "function") {
|
|
2341
|
+
throw new Error("File name validator function is required");
|
|
2342
|
+
}
|
|
2343
|
+
if (!config.fileNameVariablesExtractor || typeof config.fileNameVariablesExtractor !== "function") {
|
|
2344
|
+
throw new Error("File name variables extractor function is required");
|
|
2345
|
+
}
|
|
2346
|
+
if (config.type === "single-sheet-extraction" && (!config.columns || Object.keys(config.columns).length === 0)) {
|
|
2347
|
+
throw new Error("At least one column configuration is required for single-sheet-extraction");
|
|
2348
|
+
}
|
|
2349
|
+
if (config.type === "single-sheet-extraction") {
|
|
2350
|
+
Object.entries(config.columns).forEach(([key, column]) => {
|
|
2351
|
+
if (!column.type) {
|
|
2352
|
+
throw new Error(`Column ${key} must have a type`);
|
|
2353
|
+
}
|
|
2354
|
+
if (column.column) {
|
|
2355
|
+
const colLetter = column.column;
|
|
2356
|
+
if (!/^[A-Z]+$/i.test(colLetter)) {
|
|
2357
|
+
throw new Error(`Invalid column letter format: ${colLetter}`);
|
|
2358
|
+
}
|
|
2359
|
+
} else if (column.variableName) {
|
|
2360
|
+
} else {
|
|
2361
|
+
throw new Error(`Column ${key} must specify either 'column' or 'variableName'`);
|
|
2362
|
+
}
|
|
2363
|
+
});
|
|
2364
|
+
if (config.numberOfRowsToSkip === void 0 || config.numberOfRowsToSkip < 0) {
|
|
2365
|
+
throw new Error("Number of rows to skip must be specified and non-negative");
|
|
2366
|
+
}
|
|
2367
|
+
} else if (config.type === "processor") {
|
|
2368
|
+
if (!config.processor || typeof config.processor !== "function") {
|
|
2369
|
+
throw new Error("Processor function is required for processor type");
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
};
|
|
2373
|
+
var findAllMatchingConfigs = (filename, configs) => {
|
|
2374
|
+
const fileInfo = path.parse(filename);
|
|
2375
|
+
return configs.filter(
|
|
2376
|
+
(config) => config.fileNameValidator(fileInfo.base) && config.extension === fileInfo.ext.replace(".", "")
|
|
2377
|
+
);
|
|
2378
|
+
};
|
|
2379
|
+
async function* createExcelGenerator(data) {
|
|
2380
|
+
for (const row of data) {
|
|
2381
|
+
yield row;
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
// src/sdk/file-processing/excel/config-builder.ts
|
|
2386
|
+
var ExcelExtractionConfigBuilder = class _ExcelExtractionConfigBuilder {
|
|
2387
|
+
config = {
|
|
2388
|
+
fileNameValidator: () => true,
|
|
2389
|
+
fileNameVariablesExtractor: () => ({}),
|
|
2390
|
+
replicationMethod: "FULL_TABLE" /* FULL_TABLE */
|
|
2391
|
+
};
|
|
2392
|
+
static create() {
|
|
2393
|
+
return new _ExcelExtractionConfigBuilder();
|
|
2394
|
+
}
|
|
2395
|
+
extension(ext) {
|
|
2396
|
+
this.config.extension = ext;
|
|
2397
|
+
return this;
|
|
2398
|
+
}
|
|
2399
|
+
tableName(name) {
|
|
2400
|
+
this.config.tableName = name;
|
|
2401
|
+
return this;
|
|
2402
|
+
}
|
|
2403
|
+
fileValidator(validator) {
|
|
2404
|
+
this.config.fileNameValidator = validator;
|
|
2405
|
+
return this;
|
|
2406
|
+
}
|
|
2407
|
+
variablesExtractor(extractor) {
|
|
2408
|
+
this.config.fileNameVariablesExtractor = extractor;
|
|
2409
|
+
return this;
|
|
2410
|
+
}
|
|
2411
|
+
replicationMethod(method) {
|
|
2412
|
+
this.config.replicationMethod = method;
|
|
2413
|
+
return this;
|
|
2414
|
+
}
|
|
2415
|
+
columns(cols) {
|
|
2416
|
+
if (this.config.type === "single-sheet-extraction") {
|
|
2417
|
+
this.config.columns = cols;
|
|
2418
|
+
} else {
|
|
2419
|
+
throw new Error("Columns can only be set for single-sheet-extraction");
|
|
2420
|
+
}
|
|
2421
|
+
return this;
|
|
2422
|
+
}
|
|
2423
|
+
singleSheet(sheetName, skipRows = 0) {
|
|
2424
|
+
this.config.type = "single-sheet-extraction";
|
|
2425
|
+
if (sheetName !== void 0) {
|
|
2426
|
+
this.config.sheetName = sheetName;
|
|
2427
|
+
}
|
|
2428
|
+
this.config.numberOfRowsToSkip = skipRows;
|
|
2429
|
+
return this;
|
|
2430
|
+
}
|
|
2431
|
+
processor(processorFn) {
|
|
2432
|
+
this.config.type = "processor";
|
|
2433
|
+
this.config.processor = processorFn;
|
|
2434
|
+
return this;
|
|
2435
|
+
}
|
|
2436
|
+
build() {
|
|
2437
|
+
if (!this.config.type) {
|
|
2438
|
+
throw new Error("Configuration type must be specified (singleSheet or processor)");
|
|
2439
|
+
}
|
|
2440
|
+
if (!this.config.extension) {
|
|
2441
|
+
throw new Error("Extension must be set");
|
|
2442
|
+
}
|
|
2443
|
+
if (!this.config.tableName) {
|
|
2444
|
+
throw new Error("Table name must be set");
|
|
2445
|
+
}
|
|
2446
|
+
if (!this.config.replicationMethod) {
|
|
2447
|
+
throw new Error("Replication method must be set");
|
|
2448
|
+
}
|
|
2449
|
+
if (typeof this.config.fileNameValidator !== "function") {
|
|
2450
|
+
throw new Error("fileNameValidator must be a function");
|
|
2451
|
+
}
|
|
2452
|
+
if (typeof this.config.fileNameVariablesExtractor !== "function") {
|
|
2453
|
+
throw new Error("fileNameVariablesExtractor must be a function");
|
|
2454
|
+
}
|
|
2455
|
+
if (this.config.type === "single-sheet-extraction") {
|
|
2456
|
+
const singleSheetConfig = this.config;
|
|
2457
|
+
if (!singleSheetConfig.columns || Object.keys(singleSheetConfig.columns).length === 0) {
|
|
2458
|
+
throw new Error("Columns must be set with at least one column for single-sheet-extraction");
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
if (this.config.type === "processor") {
|
|
2462
|
+
const processorConfig = this.config;
|
|
2463
|
+
if (typeof processorConfig.processor !== "function") {
|
|
2464
|
+
throw new Error("Processor function must be set for processor configs");
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
return this.config;
|
|
2468
|
+
}
|
|
2469
|
+
};
|
|
2470
|
+
|
|
2471
|
+
// src/sdk/file-processing/csv/reader.ts
|
|
2472
|
+
import csv from "csv-parser";
|
|
2473
|
+
import { createReadStream } from "fs";
|
|
2474
|
+
import { format as format4 } from "util";
|
|
2475
|
+
import { Readable, Transform } from "stream";
|
|
2476
|
+
import * as iconv from "iconv-lite";
|
|
2477
|
+
var logPrefix = "[csvReader]";
|
|
2478
|
+
var createStripBomTransform = () => {
|
|
2479
|
+
let isFirstChunk = true;
|
|
2480
|
+
return new Transform({
|
|
2481
|
+
transform(chunk2, encoding, callback) {
|
|
2482
|
+
if (isFirstChunk) {
|
|
2483
|
+
isFirstChunk = false;
|
|
2484
|
+
if (chunk2.length >= 3 && chunk2[0] === 239 && chunk2[1] === 187 && chunk2[2] === 191) {
|
|
2485
|
+
chunk2 = chunk2.slice(3);
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
callback(null, chunk2);
|
|
2489
|
+
}
|
|
2490
|
+
});
|
|
2491
|
+
};
|
|
2492
|
+
var getStringHexInfo = (str) => {
|
|
2493
|
+
const bytes = Buffer.from(str, "utf8");
|
|
2494
|
+
const hex = bytes.toString("hex").match(/.{2}/g)?.join(" ") || "";
|
|
2495
|
+
return {
|
|
2496
|
+
original: str,
|
|
2497
|
+
length: str.length,
|
|
2498
|
+
hex,
|
|
2499
|
+
trimmed: str.trim()
|
|
2500
|
+
};
|
|
2501
|
+
};
|
|
2502
|
+
var formatHexComparison = (expected, actual) => {
|
|
2503
|
+
const expectedInfo = getStringHexInfo(expected);
|
|
2504
|
+
const actualInfo = getStringHexInfo(actual);
|
|
2505
|
+
return `
|
|
2506
|
+
Expected: "${expected}" (${expectedInfo.length} chars) [${expectedInfo.hex}]
|
|
2507
|
+
Actual: "${actual}" (${actualInfo.length} chars) [${actualInfo.hex}]`;
|
|
2508
|
+
};
|
|
2509
|
+
async function* rowGeneratorFromCsv(path2, fileConfig) {
|
|
2510
|
+
const isGeneratorConfigArray = Array.isArray(fileConfig.fields);
|
|
2511
|
+
const configKeys = isGeneratorConfigArray ? [] : Object.keys(fileConfig.fields);
|
|
2512
|
+
const csvOptions = { separator: fileConfig.separator };
|
|
2513
|
+
if (isGeneratorConfigArray) {
|
|
2514
|
+
csvOptions.headers = false;
|
|
2515
|
+
}
|
|
2516
|
+
const csvStream = csv(csvOptions);
|
|
2517
|
+
const syncedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2518
|
+
let initialReadStream;
|
|
2519
|
+
let finalInputStream;
|
|
2520
|
+
const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
|
|
2521
|
+
if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
|
|
2522
|
+
initialReadStream = createReadStream(path2);
|
|
2523
|
+
finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
|
|
2524
|
+
} else {
|
|
2525
|
+
initialReadStream = createReadStream(path2);
|
|
2526
|
+
finalInputStream = initialReadStream.pipe(createStripBomTransform());
|
|
2527
|
+
}
|
|
2528
|
+
finalInputStream.pipe(csvStream);
|
|
2529
|
+
try {
|
|
2530
|
+
for await (const row of Readable.from(csvStream)) {
|
|
2531
|
+
if (!isGeneratorConfigArray) {
|
|
2532
|
+
const rowData = fileConfig.addSyncedAtColumn ? { _wly_synced_at: syncedAt } : {};
|
|
2533
|
+
for (const key of configKeys) {
|
|
2534
|
+
const keyGenerator = fileConfig.fields[key.trim()];
|
|
2535
|
+
if (!keyGenerator) continue;
|
|
2536
|
+
rowData[keyGenerator.key] = keyGenerator.valueTransformer ? keyGenerator.valueTransformer(row[key]) : row[key];
|
|
2537
|
+
}
|
|
2538
|
+
yield rowData;
|
|
2539
|
+
} else {
|
|
2540
|
+
const rowKeys = Object.keys(row);
|
|
2541
|
+
const rowData = fileConfig.addSyncedAtColumn ? { _wly_synced_at: syncedAt } : {};
|
|
2542
|
+
for (let i = 0; i < rowKeys.length; i++) {
|
|
2543
|
+
const keyGenerator = fileConfig.fields[i];
|
|
2544
|
+
if (!keyGenerator) continue;
|
|
2545
|
+
rowData[keyGenerator.key] = keyGenerator.valueTransformer ? keyGenerator.valueTransformer(row[i]) : row[i];
|
|
2546
|
+
}
|
|
2547
|
+
yield rowData;
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
} finally {
|
|
2551
|
+
initialReadStream.destroy();
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
async function countCsvLines(filePath) {
|
|
2555
|
+
return new Promise((resolve, reject) => {
|
|
2556
|
+
let newlines = 0;
|
|
2557
|
+
createReadStream(filePath).on("data", (chunk2) => {
|
|
2558
|
+
const buf = Buffer.isBuffer(chunk2) ? chunk2 : Buffer.from(chunk2);
|
|
2559
|
+
for (let i = 0; i < buf.length; i++) {
|
|
2560
|
+
if (buf[i] === 10) newlines++;
|
|
2561
|
+
}
|
|
2562
|
+
}).on("end", () => resolve(Math.max(0, newlines - 1))).on("error", reject);
|
|
2563
|
+
});
|
|
2564
|
+
}
|
|
2565
|
+
var checkCsvHeaderRow = async (path2, fileConfig) => {
|
|
2566
|
+
return new Promise((resolve, reject) => {
|
|
2567
|
+
const colsWithParsingConfig = Object.keys(fileConfig.fields).map((key) => key.trim());
|
|
2568
|
+
console.debug(`${logPrefix} CSV Expected columns:`, colsWithParsingConfig.map((col) => {
|
|
2569
|
+
const info = getStringHexInfo(col);
|
|
2570
|
+
return `"${info.original}" [${info.hex}]`;
|
|
2571
|
+
}));
|
|
2572
|
+
const isGeneratorConfigArray = Array.isArray(fileConfig.fields);
|
|
2573
|
+
const csvOptions2 = { separator: fileConfig.separator };
|
|
2574
|
+
if (isGeneratorConfigArray) {
|
|
2575
|
+
csvOptions2.headers = false;
|
|
2576
|
+
}
|
|
2577
|
+
const csvStream = csv(csvOptions2);
|
|
2578
|
+
let initialReadStream;
|
|
2579
|
+
let finalInputStream;
|
|
2580
|
+
const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
|
|
2581
|
+
if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
|
|
2582
|
+
initialReadStream = createReadStream(path2);
|
|
2583
|
+
finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
|
|
2584
|
+
} else {
|
|
2585
|
+
initialReadStream = createReadStream(path2);
|
|
2586
|
+
finalInputStream = initialReadStream.pipe(createStripBomTransform());
|
|
2587
|
+
}
|
|
2588
|
+
let isFirstLine = true;
|
|
2589
|
+
finalInputStream.pipe(csvStream).on("data", (row) => {
|
|
2590
|
+
if (isFirstLine) {
|
|
2591
|
+
let headers = [];
|
|
2592
|
+
if (isGeneratorConfigArray) {
|
|
2593
|
+
headers = Object.values(row).filter((r) => !!r);
|
|
2594
|
+
} else {
|
|
2595
|
+
headers = Object.keys(row).filter((r) => !!r);
|
|
2596
|
+
}
|
|
2597
|
+
console.debug(`${logPrefix} CSV Detected headers:`, headers.map((h, idx) => {
|
|
2598
|
+
const info = getStringHexInfo(h);
|
|
2599
|
+
return `[${idx}]: "${info.original}" [${info.hex}]`;
|
|
2600
|
+
}));
|
|
2601
|
+
if (isGeneratorConfigArray) {
|
|
2602
|
+
colsWithParsingConfig.forEach((col, i) => {
|
|
2603
|
+
if (col !== headers[i]) {
|
|
2604
|
+
const hexComparison = formatHexComparison(col, headers[i] || "<undefined>");
|
|
2605
|
+
console.error(`${logPrefix} CSV Header Mismatch:${hexComparison}`);
|
|
2606
|
+
throw new Error(format4(
|
|
2607
|
+
`CSV header mismatch at position %s: expected '%s' but got '%s' in file %s`,
|
|
2608
|
+
i,
|
|
2609
|
+
col,
|
|
2610
|
+
headers[i] || "<undefined>",
|
|
2611
|
+
path2
|
|
2612
|
+
));
|
|
2613
|
+
}
|
|
2614
|
+
});
|
|
2615
|
+
} else {
|
|
2616
|
+
colsWithParsingConfig.forEach((col) => {
|
|
2617
|
+
if (!headers.includes(col)) {
|
|
2618
|
+
const colInfo = getStringHexInfo(col);
|
|
2619
|
+
console.error(`CSV Missing column: "${col}" [${colInfo.hex}]`);
|
|
2620
|
+
console.error(`Available headers:`, headers.map((h) => `"${h}" [${getStringHexInfo(h).hex}]`));
|
|
2621
|
+
const trimmedMatch = headers.find((h) => h.trim() === col.trim());
|
|
2622
|
+
if (trimmedMatch) {
|
|
2623
|
+
console.error(`Potential trimmed match: "${trimmedMatch}" [${getStringHexInfo(trimmedMatch).hex}]`);
|
|
2624
|
+
}
|
|
2625
|
+
throw new Error(format4(
|
|
2626
|
+
`CSV missing expected column '%s' in file: %s`,
|
|
2627
|
+
col,
|
|
2628
|
+
path2
|
|
2629
|
+
));
|
|
2630
|
+
}
|
|
2631
|
+
});
|
|
2632
|
+
}
|
|
2633
|
+
isFirstLine = false;
|
|
2634
|
+
} else {
|
|
2635
|
+
initialReadStream.destroy();
|
|
2636
|
+
}
|
|
2637
|
+
}).on("error", (error) => {
|
|
2638
|
+
reject(new Error(`Error processing CSV file: ${error.message}`));
|
|
2639
|
+
});
|
|
2640
|
+
initialReadStream.on("close", () => {
|
|
2641
|
+
resolve();
|
|
2642
|
+
});
|
|
2643
|
+
});
|
|
2644
|
+
};
|
|
2645
|
+
|
|
2646
|
+
// src/sdk/file-processing/csv/writer.ts
|
|
2647
|
+
import { createObjectCsvWriter } from "csv-writer";
|
|
2648
|
+
import { format as format5 } from "util";
|
|
2649
|
+
var logPrefix2 = "[csvWriter]";
|
|
2650
|
+
var writeDataToCsv = async (fileName, data) => {
|
|
2651
|
+
try {
|
|
2652
|
+
const firstRow = data[0];
|
|
2653
|
+
if (!firstRow) {
|
|
2654
|
+
console.log(`${logPrefix2} No data to write to CSV file=%s`, fileName);
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2657
|
+
const csvWriter = createObjectCsvWriter({
|
|
2658
|
+
path: fileName,
|
|
2659
|
+
header: Object.keys(firstRow).map((col) => {
|
|
2660
|
+
return { id: col, title: col };
|
|
2661
|
+
})
|
|
2662
|
+
});
|
|
2663
|
+
await csvWriter.writeRecords(data);
|
|
2664
|
+
console.log(`${logPrefix2} CSV file=%s written successfully`, fileName);
|
|
2665
|
+
} catch (err) {
|
|
2666
|
+
if (err instanceof Error) {
|
|
2667
|
+
throw new Error(format5(`error while writing csv file=%s, err: %s`, fileName, err.message));
|
|
2668
|
+
}
|
|
2669
|
+
throw err;
|
|
2670
|
+
}
|
|
2671
|
+
};
|
|
2672
|
+
var writeGeneratorToCSV = async (generator, outputFileName) => {
|
|
2673
|
+
try {
|
|
2674
|
+
let rowCount = 0;
|
|
2675
|
+
const firstDataRow = (await generator.next()).value;
|
|
2676
|
+
if (!firstDataRow) {
|
|
2677
|
+
console.log(`${logPrefix2} [%s] first data row of csv empty, skipping`, outputFileName);
|
|
2678
|
+
return 0;
|
|
2679
|
+
}
|
|
2680
|
+
console.log(`${logPrefix2} [%s] inferring headers from first data row`, outputFileName);
|
|
2681
|
+
const headers = Object.keys(firstDataRow).map((col) => {
|
|
2682
|
+
return {
|
|
2683
|
+
id: col,
|
|
2684
|
+
title: col
|
|
2685
|
+
};
|
|
2686
|
+
});
|
|
2687
|
+
const csvWriter = createObjectCsvWriter({
|
|
2688
|
+
path: outputFileName,
|
|
2689
|
+
header: headers,
|
|
2690
|
+
alwaysQuote: true
|
|
2691
|
+
});
|
|
2692
|
+
console.log(`${logPrefix2} [%s] writing csv`, outputFileName);
|
|
2693
|
+
let batch = [firstDataRow];
|
|
2694
|
+
for await (const data of generator) {
|
|
2695
|
+
batch.push(data);
|
|
2696
|
+
rowCount++;
|
|
2697
|
+
if (batch.length > 1e4) {
|
|
2698
|
+
try {
|
|
2699
|
+
await csvWriter.writeRecords(batch);
|
|
2700
|
+
} catch (err) {
|
|
2701
|
+
if (err instanceof Error) {
|
|
2702
|
+
throw new Error(
|
|
2703
|
+
format5(
|
|
2704
|
+
`error while writing batch to csv batch (first 3 records)=%j, err: %s`,
|
|
2705
|
+
batch.slice(0, 3),
|
|
2706
|
+
err.message
|
|
2707
|
+
)
|
|
2708
|
+
);
|
|
2709
|
+
}
|
|
2710
|
+
throw err;
|
|
2711
|
+
}
|
|
2712
|
+
batch = [];
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
await csvWriter.writeRecords(batch);
|
|
2716
|
+
console.log(`${logPrefix2} [%s] csv written successfully with %d rows`, outputFileName, rowCount);
|
|
2717
|
+
return rowCount;
|
|
2718
|
+
} catch (err) {
|
|
2719
|
+
if (err instanceof Error) {
|
|
2720
|
+
throw new Error(format5(`error while writing csv file=%s, err: %s`, outputFileName, err.message));
|
|
2721
|
+
}
|
|
2722
|
+
throw err;
|
|
2723
|
+
}
|
|
2724
|
+
};
|
|
2725
|
+
|
|
2726
|
+
// src/sdk/file-processing/file-stream.ts
|
|
2727
|
+
import XLSX2 from "xlsx";
|
|
2728
|
+
var FileStream = class extends Stream {
|
|
2729
|
+
localFilePaths;
|
|
2730
|
+
constructor(config, localFilePath, tapState, target) {
|
|
2731
|
+
super(config, tapState, target);
|
|
2732
|
+
this.localFilePaths = Array.isArray(localFilePath) ? localFilePath : [localFilePath];
|
|
2733
|
+
this.streamId = config.streamId;
|
|
2734
|
+
this.primaryKey = config.primaryKeys;
|
|
2735
|
+
this.replicationMethod = config.replicationMethod;
|
|
2736
|
+
}
|
|
2737
|
+
/**
|
|
2738
|
+
* Build JSON Schema from the file config's field definitions.
|
|
2739
|
+
*/
|
|
2740
|
+
async getSchema() {
|
|
2741
|
+
if (this.config.format === "EXCEL" /* EXCEL */) {
|
|
2742
|
+
const excelConfig = this.config.excel;
|
|
2743
|
+
if (excelConfig.type === "single-sheet-extraction") {
|
|
2744
|
+
const jsonSchema = excelFieldsToJsonSchema(excelConfig.columns);
|
|
2745
|
+
return { jsonSchema };
|
|
2746
|
+
}
|
|
2747
|
+
return void 0;
|
|
2748
|
+
}
|
|
2749
|
+
if (this.config.format === "CSV" /* CSV */) {
|
|
2750
|
+
const jsonSchema = csvFieldsToJsonSchema(this.config.csv.fields);
|
|
2751
|
+
return { jsonSchema };
|
|
2752
|
+
}
|
|
2753
|
+
throw new Error(`FileStream: No valid format config for stream ${this.streamId}`);
|
|
2754
|
+
}
|
|
2755
|
+
/**
|
|
2756
|
+
* Yield records from all file(s).
|
|
2757
|
+
* When multiple file paths are provided, records are yielded sequentially from each file.
|
|
2758
|
+
* Pre-counts total rows across all files to enable percentage progress logging.
|
|
2759
|
+
*/
|
|
2760
|
+
async *_getRecords() {
|
|
2761
|
+
let total = 0;
|
|
2762
|
+
for (const filePath of this.localFilePaths) {
|
|
2763
|
+
if (this.config.format === "EXCEL" /* EXCEL */) {
|
|
2764
|
+
const data = await extractExcelRows(filePath, this.config.excel);
|
|
2765
|
+
total += data.length;
|
|
2766
|
+
} else if (this.config.format === "CSV" /* CSV */) {
|
|
2767
|
+
total += await countCsvLines(filePath);
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
this.totalRows = total;
|
|
2771
|
+
for (const filePath of this.localFilePaths) {
|
|
2772
|
+
if (this.config.format === "EXCEL" /* EXCEL */) {
|
|
2773
|
+
const data = await extractExcelRows(filePath, this.config.excel);
|
|
2774
|
+
for (const row of data) {
|
|
2775
|
+
yield row;
|
|
2776
|
+
}
|
|
2777
|
+
} else if (this.config.format === "CSV" /* CSV */) {
|
|
2778
|
+
yield* rowGeneratorFromCsv(filePath, this.config.csv);
|
|
2779
|
+
} else {
|
|
2780
|
+
throw new Error(`FileStream: Unsupported format for stream ${this.streamId}`);
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
};
|
|
2785
|
+
function createExcelStreamConfig(excelConfig, fileName, localFilePath) {
|
|
2786
|
+
if (excelConfig.type === "single-sheet-extraction") {
|
|
2787
|
+
const primaryKeys = extractPrimaryKeysFromExcelFields(excelConfig.columns);
|
|
2788
|
+
let tableName2;
|
|
2789
|
+
if (typeof excelConfig.tableName === "function") {
|
|
2790
|
+
const variables = excelConfig.fileNameVariablesExtractor(fileName);
|
|
2791
|
+
tableName2 = excelConfig.tableName(fileName, void 0, variables);
|
|
2792
|
+
} else {
|
|
2793
|
+
tableName2 = excelConfig.tableName;
|
|
2794
|
+
}
|
|
2795
|
+
return {
|
|
2796
|
+
format: "EXCEL" /* EXCEL */,
|
|
2797
|
+
streamId: tableName2,
|
|
2798
|
+
replicationMethod: excelConfig.replicationMethod,
|
|
2799
|
+
primaryKeys,
|
|
2800
|
+
excel: excelConfig
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2803
|
+
let tableName;
|
|
2804
|
+
if (typeof excelConfig.tableName === "function") {
|
|
2805
|
+
if (!localFilePath) {
|
|
2806
|
+
throw new Error("localFilePath is required for processor configs with dynamic table names");
|
|
2807
|
+
}
|
|
2808
|
+
const workbook = XLSX2.readFile(localFilePath, { dense: true });
|
|
2809
|
+
const variables = excelConfig.fileNameVariablesExtractor(fileName, workbook);
|
|
2810
|
+
tableName = excelConfig.tableName(fileName, workbook, variables);
|
|
2811
|
+
} else {
|
|
2812
|
+
tableName = excelConfig.tableName;
|
|
2813
|
+
}
|
|
2814
|
+
return {
|
|
2815
|
+
format: "EXCEL" /* EXCEL */,
|
|
2816
|
+
streamId: tableName,
|
|
2817
|
+
replicationMethod: excelConfig.replicationMethod,
|
|
2818
|
+
primaryKeys: [],
|
|
2819
|
+
excel: excelConfig
|
|
2820
|
+
};
|
|
2821
|
+
}
|
|
2822
|
+
function createCsvStreamConfig(streamId, csvConfig, options) {
|
|
2823
|
+
return {
|
|
2824
|
+
format: "CSV" /* CSV */,
|
|
2825
|
+
streamId,
|
|
2826
|
+
replicationMethod: options?.replicationMethod ?? "FULL_TABLE" /* FULL_TABLE */,
|
|
2827
|
+
primaryKeys: options?.primaryKeys ?? extractPrimaryKeysFromCsvConfig(csvConfig.fields),
|
|
2828
|
+
csv: csvConfig
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
async function processFileStreams(entries, tapState, target) {
|
|
2832
|
+
for (const entry of entries) {
|
|
2833
|
+
const stream = new FileStream(entry.config, entry.filePath, tapState, target);
|
|
2834
|
+
await stream.sync();
|
|
2835
|
+
}
|
|
2836
|
+
await target.complete();
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
// src/sdk/file-processing/csv/config-builder.ts
|
|
2840
|
+
var CsvExtractionConfigBuilder = class _CsvExtractionConfigBuilder {
|
|
2841
|
+
_separator = ",";
|
|
2842
|
+
_encoding;
|
|
2843
|
+
_addSyncedAtColumn;
|
|
2844
|
+
_fields;
|
|
2845
|
+
_fieldsMode;
|
|
2846
|
+
_replicationMethod;
|
|
2847
|
+
_primaryKeys;
|
|
2848
|
+
static create() {
|
|
2849
|
+
return new _CsvExtractionConfigBuilder();
|
|
2850
|
+
}
|
|
2851
|
+
separator(sep) {
|
|
2852
|
+
this._separator = sep;
|
|
2853
|
+
return this;
|
|
2854
|
+
}
|
|
2855
|
+
encoding(enc) {
|
|
2856
|
+
this._encoding = enc;
|
|
2857
|
+
return this;
|
|
2858
|
+
}
|
|
2859
|
+
addSyncedAtColumn(enabled = true) {
|
|
2860
|
+
this._addSyncedAtColumn = enabled;
|
|
2861
|
+
return this;
|
|
2862
|
+
}
|
|
2863
|
+
fieldsFromDict(mapping) {
|
|
2864
|
+
if (this._fieldsMode === "array") {
|
|
2865
|
+
throw new Error("Cannot use fieldsFromDict after fieldsFromArray");
|
|
2866
|
+
}
|
|
2867
|
+
this._fieldsMode = "dict";
|
|
2868
|
+
this._fields = mapping;
|
|
2869
|
+
return this;
|
|
2870
|
+
}
|
|
2871
|
+
fieldsFromArray(fields) {
|
|
2872
|
+
if (this._fieldsMode === "dict") {
|
|
2873
|
+
throw new Error("Cannot use fieldsFromArray after fieldsFromDict");
|
|
2874
|
+
}
|
|
2875
|
+
this._fieldsMode = "array";
|
|
2876
|
+
this._fields = fields;
|
|
2877
|
+
return this;
|
|
2878
|
+
}
|
|
2879
|
+
replicationMethod(method) {
|
|
2880
|
+
this._replicationMethod = method;
|
|
2881
|
+
return this;
|
|
2882
|
+
}
|
|
2883
|
+
primaryKeys(keys) {
|
|
2884
|
+
this._primaryKeys = keys;
|
|
2885
|
+
return this;
|
|
2886
|
+
}
|
|
2887
|
+
build() {
|
|
2888
|
+
if (!this._fields) {
|
|
2889
|
+
throw new Error("Fields must be configured (use fieldsFromDict or fieldsFromArray)");
|
|
2890
|
+
}
|
|
2891
|
+
if (Array.isArray(this._fields) && this._fields.length === 0) {
|
|
2892
|
+
throw new Error("Fields must not be empty");
|
|
2893
|
+
}
|
|
2894
|
+
if (!Array.isArray(this._fields) && Object.keys(this._fields).length === 0) {
|
|
2895
|
+
throw new Error("Fields must not be empty");
|
|
2896
|
+
}
|
|
2897
|
+
const config = {
|
|
2898
|
+
separator: this._separator,
|
|
2899
|
+
fields: this._fields
|
|
2900
|
+
};
|
|
2901
|
+
if (this._encoding !== void 0) {
|
|
2902
|
+
config.encoding = this._encoding;
|
|
2903
|
+
}
|
|
2904
|
+
if (this._addSyncedAtColumn !== void 0) {
|
|
2905
|
+
config.addSyncedAtColumn = this._addSyncedAtColumn;
|
|
2906
|
+
}
|
|
2907
|
+
return config;
|
|
2908
|
+
}
|
|
2909
|
+
buildStreamConfig(streamId) {
|
|
2910
|
+
const csvConfig = this.build();
|
|
2911
|
+
const options = {};
|
|
2912
|
+
if (this._replicationMethod !== void 0) {
|
|
2913
|
+
options.replicationMethod = this._replicationMethod;
|
|
2914
|
+
}
|
|
2915
|
+
if (this._primaryKeys !== void 0) {
|
|
2916
|
+
options.primaryKeys = this._primaryKeys;
|
|
2917
|
+
}
|
|
2918
|
+
return createCsvStreamConfig(streamId, csvConfig, options);
|
|
2919
|
+
}
|
|
2920
|
+
};
|
|
2921
|
+
|
|
2922
|
+
// src/sdk/file-processing/file-tap.ts
|
|
2923
|
+
var FileTap = class _FileTap extends Tap {
|
|
2924
|
+
entries;
|
|
2925
|
+
constructor(target, config, stateProvider, entries) {
|
|
2926
|
+
super(target, config, stateProvider);
|
|
2927
|
+
this.entries = entries;
|
|
2928
|
+
}
|
|
2929
|
+
/**
|
|
2930
|
+
* Convenience constructor for the common case where all configs share the same file path.
|
|
2931
|
+
*/
|
|
2932
|
+
static fromConfigs(target, config, stateProvider, fileConfigs, sharedFilePath) {
|
|
2933
|
+
const entries = fileConfigs.map((c) => ({
|
|
2934
|
+
config: c,
|
|
2935
|
+
filePath: sharedFilePath
|
|
2936
|
+
}));
|
|
2937
|
+
return new _FileTap(target, config, stateProvider, entries);
|
|
2938
|
+
}
|
|
2939
|
+
async init() {
|
|
2940
|
+
for (const entry of this.entries) {
|
|
2941
|
+
const stream = new FileStream(
|
|
2942
|
+
entry.config,
|
|
2943
|
+
entry.filePath,
|
|
2944
|
+
this.tapState,
|
|
2945
|
+
this.target
|
|
2946
|
+
);
|
|
2947
|
+
this.streams.push(stream);
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
};
|
|
2951
|
+
|
|
2952
|
+
// src/sdk/file-processing/file-patterns.ts
|
|
2953
|
+
var VariableExtractors = class {
|
|
2954
|
+
/**
|
|
2955
|
+
* Returns the filename as a variable.
|
|
2956
|
+
*/
|
|
2957
|
+
static filename() {
|
|
2958
|
+
return (filename) => ({
|
|
2959
|
+
fileName: filename
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2962
|
+
/**
|
|
2963
|
+
* Extracts named capture groups from a regex pattern.
|
|
2964
|
+
*/
|
|
2965
|
+
static regex(pattern) {
|
|
2966
|
+
return (filename) => {
|
|
2967
|
+
const match = filename.match(pattern);
|
|
2968
|
+
return match?.groups ?? {};
|
|
2969
|
+
};
|
|
2970
|
+
}
|
|
2971
|
+
/**
|
|
2972
|
+
* Combines multiple extraction functions.
|
|
2973
|
+
*/
|
|
2974
|
+
static combine(...extractors) {
|
|
2975
|
+
return (filename) => {
|
|
2976
|
+
return extractors.reduce((acc, extractor) => {
|
|
2977
|
+
return { ...acc, ...extractor(filename) };
|
|
2978
|
+
}, {});
|
|
2979
|
+
};
|
|
2980
|
+
}
|
|
2981
|
+
};
|
|
2982
|
+
var FilePatterns = class {
|
|
2983
|
+
/**
|
|
2984
|
+
* Creates a validator for files starting with a specific prefix (case-insensitive).
|
|
2985
|
+
*/
|
|
2986
|
+
static startsWith(prefix) {
|
|
2987
|
+
return (filename) => filename.toLowerCase().startsWith(prefix.toLowerCase());
|
|
2988
|
+
}
|
|
2989
|
+
/**
|
|
2990
|
+
* Creates a validator for files matching a regex pattern.
|
|
2991
|
+
*/
|
|
2992
|
+
static regex(pattern) {
|
|
2993
|
+
return (filename) => pattern.test(filename);
|
|
2994
|
+
}
|
|
2995
|
+
/**
|
|
2996
|
+
* Combines multiple validators with AND logic.
|
|
2997
|
+
*/
|
|
2998
|
+
static and(...validators) {
|
|
2999
|
+
return (filename) => validators.every((validator) => validator(filename));
|
|
3000
|
+
}
|
|
3001
|
+
/**
|
|
3002
|
+
* Combines multiple validators with OR logic.
|
|
3003
|
+
*/
|
|
3004
|
+
static or(...validators) {
|
|
3005
|
+
return (filename) => validators.some((validator) => validator(filename));
|
|
3006
|
+
}
|
|
3007
|
+
};
|
|
3008
|
+
|
|
3009
|
+
// src/services/sftp.ts
|
|
3010
|
+
import Client from "ssh2-sftp-client";
|
|
3011
|
+
import { default as default2 } from "ssh2-sftp-client";
|
|
3012
|
+
var SftpService = new Client();
|
|
3013
|
+
|
|
3014
|
+
// src/services/cloud-storage.ts
|
|
3015
|
+
import { Storage } from "@google-cloud/storage";
|
|
3016
|
+
import { format as format6 } from "util";
|
|
3017
|
+
import * as pathModule from "path";
|
|
3018
|
+
import { existsSync, mkdirSync, unlinkSync as unlinkSync2 } from "fs";
|
|
3019
|
+
import { randomUUID } from "crypto";
|
|
3020
|
+
var logPrefix3 = "[CloudStorageService]";
|
|
3021
|
+
var tmpDir = "tmp";
|
|
3022
|
+
var CloudStorageService = class {
|
|
3023
|
+
storage;
|
|
3024
|
+
bucket;
|
|
3025
|
+
processedSuffix;
|
|
3026
|
+
supportedExtensions;
|
|
3027
|
+
path;
|
|
3028
|
+
constructor(bucketName, path2, opts) {
|
|
3029
|
+
this.storage = new Storage({ retryOptions: { autoRetry: true, maxRetries: 20 } });
|
|
3030
|
+
this.bucket = this.storage.bucket(bucketName);
|
|
3031
|
+
this.path = path2;
|
|
3032
|
+
this.processedSuffix = opts?.processedSuffix ?? ".processed";
|
|
3033
|
+
this.supportedExtensions = opts?.supportedExtensions ?? [];
|
|
3034
|
+
}
|
|
3035
|
+
/**
|
|
3036
|
+
* Lists all files in the bucket with an optional prefix.
|
|
3037
|
+
*/
|
|
3038
|
+
async listFiles(prefix) {
|
|
3039
|
+
const effectivePrefix = prefix || this.path;
|
|
3040
|
+
const [files] = await this.bucket.getFiles(
|
|
3041
|
+
effectivePrefix ? { prefix: effectivePrefix } : void 0
|
|
3042
|
+
);
|
|
3043
|
+
return files.map((f) => f.name);
|
|
3044
|
+
}
|
|
3045
|
+
/**
|
|
3046
|
+
* Returns files that haven't been marked as processed.
|
|
3047
|
+
* Filters by supported extensions if configured.
|
|
3048
|
+
*/
|
|
3049
|
+
async getUnprocessedFiles() {
|
|
3050
|
+
const allFiles = await this.listFiles();
|
|
3051
|
+
const markerFiles = new Set(
|
|
3052
|
+
allFiles.filter((f) => f.endsWith(this.processedSuffix)).map((f) => f.replace(this.processedSuffix, ""))
|
|
3053
|
+
);
|
|
3054
|
+
return allFiles.filter((f) => {
|
|
3055
|
+
if (f.endsWith(this.processedSuffix)) return false;
|
|
3056
|
+
if (f.endsWith("/")) return false;
|
|
3057
|
+
if (this.supportedExtensions.length > 0) {
|
|
3058
|
+
const ext = pathModule.extname(f).toLowerCase();
|
|
3059
|
+
if (!this.supportedExtensions.includes(ext)) return false;
|
|
3060
|
+
}
|
|
3061
|
+
return !markerFiles.has(f);
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
3064
|
+
/**
|
|
3065
|
+
* Creates a marker file to indicate a file has been processed.
|
|
3066
|
+
*/
|
|
3067
|
+
async createMarkerFile(fileName) {
|
|
3068
|
+
const markerFileName = `${fileName}${this.processedSuffix}`;
|
|
3069
|
+
try {
|
|
3070
|
+
const file = this.bucket.file(markerFileName);
|
|
3071
|
+
await file.save(format6("Marked file %s as processed", fileName));
|
|
3072
|
+
logger.info(`${logPrefix3} Marker file ${markerFileName} created successfully.`);
|
|
3073
|
+
} catch (err) {
|
|
3074
|
+
if (err instanceof Error) {
|
|
3075
|
+
throw new Error(format6(`error while writing marker file=%s, err:%s`, markerFileName, err.message));
|
|
3076
|
+
}
|
|
3077
|
+
throw err;
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
/**
|
|
3081
|
+
* Downloads a file from GCS to a local tmp directory.
|
|
3082
|
+
* Returns the local path of the downloaded file.
|
|
3083
|
+
*/
|
|
3084
|
+
async downloadFile(filePath, fileName) {
|
|
3085
|
+
const file = this.bucket.file(filePath);
|
|
3086
|
+
const tmpDirPath = pathModule.join(process.cwd(), tmpDir);
|
|
3087
|
+
if (!existsSync(tmpDirPath)) {
|
|
3088
|
+
mkdirSync(tmpDirPath);
|
|
3089
|
+
}
|
|
3090
|
+
const destFilename = pathModule.join(process.cwd(), tmpDir, fileName);
|
|
3091
|
+
try {
|
|
3092
|
+
if (existsSync(destFilename)) {
|
|
3093
|
+
unlinkSync2(destFilename);
|
|
3094
|
+
}
|
|
3095
|
+
await file.download({ destination: destFilename });
|
|
3096
|
+
logger.info(`${logPrefix3} Downloaded ${fileName} to ${destFilename}`);
|
|
3097
|
+
return destFilename;
|
|
3098
|
+
} catch (err) {
|
|
3099
|
+
if (err instanceof Error) {
|
|
3100
|
+
throw new Error(format6(
|
|
3101
|
+
`can't download file=%s from bucket, err:%s`,
|
|
3102
|
+
fileName,
|
|
3103
|
+
err.message
|
|
3104
|
+
));
|
|
3105
|
+
}
|
|
3106
|
+
throw err;
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
/**
|
|
3110
|
+
* Uploads a local file to GCS.
|
|
3111
|
+
*/
|
|
3112
|
+
async uploadFile(localPath, destPath) {
|
|
3113
|
+
try {
|
|
3114
|
+
logger.info(`${logPrefix3} preparing to upload '%s' to '%s'`, localPath, destPath);
|
|
3115
|
+
const [file] = await this.bucket.upload(localPath, { destination: destPath });
|
|
3116
|
+
logger.info(`${logPrefix3} file %s has been successfully uploaded into %s`, localPath, destPath);
|
|
3117
|
+
return file;
|
|
3118
|
+
} catch (err) {
|
|
3119
|
+
if (err instanceof Error) {
|
|
3120
|
+
throw new Error(format6(`error while uploading file file=%s into path=%s, err:%s`, localPath, destPath, err.message));
|
|
3121
|
+
}
|
|
3122
|
+
throw err;
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
/**
|
|
3126
|
+
* Reads a GCS object and returns its contents as a UTF-8 string.
|
|
3127
|
+
*/
|
|
3128
|
+
async readObjectAsString(objectPath) {
|
|
3129
|
+
try {
|
|
3130
|
+
await this.bucket.get({ autoCreate: false });
|
|
3131
|
+
const fileRef = this.bucket.file(objectPath);
|
|
3132
|
+
const [exists] = await fileRef.exists();
|
|
3133
|
+
if (!exists) {
|
|
3134
|
+
throw new Error(`GCS object not found: gs://${this.bucket.name}/${objectPath}`);
|
|
3135
|
+
}
|
|
3136
|
+
const [contents] = await fileRef.download();
|
|
3137
|
+
return contents.toString("utf8");
|
|
3138
|
+
} catch (err) {
|
|
3139
|
+
throw new Error(format6(`error reading GCS object gs://${this.bucket.name}/${objectPath}, err: %s`, err?.message));
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
/**
|
|
3143
|
+
* Writes a string to a GCS object with JSON content type.
|
|
3144
|
+
*/
|
|
3145
|
+
async writeStringObject(objectPath, contents) {
|
|
3146
|
+
try {
|
|
3147
|
+
await this.bucket.get({ autoCreate: false });
|
|
3148
|
+
const fileRef = this.bucket.file(objectPath);
|
|
3149
|
+
await fileRef.save(contents, { contentType: "application/json" });
|
|
3150
|
+
logger.info(`Uploaded object to gs://${this.bucket.name}/${objectPath}`);
|
|
3151
|
+
} catch (err) {
|
|
3152
|
+
throw new Error(format6(`error writing GCS object gs://${this.bucket.name}/${objectPath}, err: %s`, err?.message));
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
/**
|
|
3156
|
+
* Uploads a local file to the bucket with a unique name based on prefix, streamId, and UUID.
|
|
3157
|
+
* Files are stored under `<prefix>/<run-id>/` when the RUN_ID env var is set,
|
|
3158
|
+
* or `<prefix>/default/` otherwise.
|
|
3159
|
+
* Returns the GCS File reference.
|
|
3160
|
+
*/
|
|
3161
|
+
async uploadFileWithUniqueName(filePath, prefix, streamId) {
|
|
3162
|
+
try {
|
|
3163
|
+
await this.bucket.get({ autoCreate: false });
|
|
3164
|
+
const runFolder = process.env.RUN_ID ?? "default";
|
|
3165
|
+
const destinationFileName = `${prefix}/${runFolder}/${streamId}-${randomUUID()}.jsonnl`;
|
|
3166
|
+
await this.bucket.upload(filePath, {
|
|
3167
|
+
destination: destinationFileName
|
|
3168
|
+
});
|
|
3169
|
+
logger.info(`Uploaded ${filePath} into ${this.bucket.name}/${destinationFileName} GCS File`);
|
|
3170
|
+
return this.bucket.file(destinationFileName);
|
|
3171
|
+
} catch (err) {
|
|
3172
|
+
logger.error(`Issue when uploading file into GCS bucket for stream: ${streamId}
|
|
3173
|
+
|
|
3174
|
+
Error: ${err.message}
|
|
3175
|
+
Stack: ${err.stack}
|
|
3176
|
+
Code: ${err.code}
|
|
3177
|
+
`);
|
|
3178
|
+
throw err;
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
};
|
|
3182
|
+
|
|
3183
|
+
// src/services/zip.ts
|
|
3184
|
+
import * as fse from "fs-extra";
|
|
3185
|
+
import decompress from "decompress";
|
|
3186
|
+
var unzip = async (zipFilePath, extractedPath) => {
|
|
3187
|
+
await fse.ensureDir(extractedPath);
|
|
3188
|
+
await decompress(zipFilePath, extractedPath);
|
|
3189
|
+
return extractedPath;
|
|
3190
|
+
};
|
|
3191
|
+
|
|
2138
3192
|
// src/targets/bigquery/helpers.ts
|
|
2139
3193
|
var safeColumnName = (key) => {
|
|
2140
3194
|
let returnString = key;
|
|
@@ -2171,8 +3225,8 @@ import Decimal from "decimal.js";
|
|
|
2171
3225
|
import dayjs5 from "dayjs";
|
|
2172
3226
|
var validateDateRange = (record, schema) => {
|
|
2173
3227
|
Object.keys(schema).forEach((key) => {
|
|
2174
|
-
const { format:
|
|
2175
|
-
if (
|
|
3228
|
+
const { format: format8 } = schema[key];
|
|
3229
|
+
if (format8 === "date-time") {
|
|
2176
3230
|
if (record[key]) {
|
|
2177
3231
|
const formattedDate = dayjs5(record[key]).format(defaultDateTimeFormat);
|
|
2178
3232
|
const isNotTooMuchInTheFuture = dayjs5(record[key]).isBefore(dayjs5("9999-12-31 23:59:59.999999"));
|
|
@@ -2256,96 +3310,6 @@ var BqClientHolder = class {
|
|
|
2256
3310
|
}
|
|
2257
3311
|
};
|
|
2258
3312
|
|
|
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
3313
|
// src/targets/bigquery/service/dbSync.ts
|
|
2350
3314
|
import chunk from "lodash/chunk.js";
|
|
2351
3315
|
import { Semaphore as Semaphore2 } from "async-mutex";
|
|
@@ -2386,21 +3350,21 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
|
|
|
2386
3350
|
safeColumnName = safeColumnName;
|
|
2387
3351
|
getWarehouseTypeFromJSONSchema = (definition) => {
|
|
2388
3352
|
const type = definition.type;
|
|
2389
|
-
const
|
|
3353
|
+
const format8 = definition.format;
|
|
2390
3354
|
let typeArr;
|
|
2391
3355
|
if (type instanceof Array) {
|
|
2392
3356
|
typeArr = type;
|
|
2393
3357
|
} else {
|
|
2394
3358
|
typeArr = [type];
|
|
2395
3359
|
}
|
|
2396
|
-
if (
|
|
2397
|
-
switch (
|
|
3360
|
+
if (format8) {
|
|
3361
|
+
switch (format8) {
|
|
2398
3362
|
case "date-time":
|
|
2399
3363
|
return "TIMESTAMP";
|
|
2400
3364
|
case "json":
|
|
2401
3365
|
return "STRING";
|
|
2402
3366
|
default:
|
|
2403
|
-
throw new Error(`StreamId: ${this.streamId} - Unsupported format: ${
|
|
3367
|
+
throw new Error(`StreamId: ${this.streamId} - Unsupported format: ${format8}`);
|
|
2404
3368
|
}
|
|
2405
3369
|
} else if (typeArr.includes("number")) {
|
|
2406
3370
|
return "NUMERIC";
|
|
@@ -2496,6 +3460,13 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
|
|
|
2496
3460
|
...this.getMergeQueries()
|
|
2497
3461
|
];
|
|
2498
3462
|
};
|
|
3463
|
+
getAppendQueries = () => {
|
|
3464
|
+
const columnUnsafeToSafeMapping = this.renamedColumnStore.getUnsafeToSafeColumnMapping(this.streamId);
|
|
3465
|
+
const columns = Object.keys(columnUnsafeToSafeMapping).map((unsafeKey) => `\`${columnUnsafeToSafeMapping[unsafeKey]}\``).join(`, `);
|
|
3466
|
+
return [
|
|
3467
|
+
`INSERT INTO ${this.sqlTableId} (${columns}) SELECT ${columns} FROM ${this.sqlStagingTableId};`
|
|
3468
|
+
];
|
|
3469
|
+
};
|
|
2499
3470
|
createDatabaseAndSchemaIfNotExists = async (retryCount) => {
|
|
2500
3471
|
try {
|
|
2501
3472
|
await semaphore2.runExclusive(async () => {
|
|
@@ -2620,11 +3591,11 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
|
|
|
2620
3591
|
};
|
|
2621
3592
|
loadStreamInStagingArea = async (localFilePath) => {
|
|
2622
3593
|
try {
|
|
2623
|
-
const
|
|
2624
|
-
|
|
2625
|
-
|
|
3594
|
+
const gcsService = new CloudStorageService(this.config.loading_deck_gcs_bucket_name);
|
|
3595
|
+
const file = await gcsService.uploadFileWithUniqueName(
|
|
3596
|
+
localFilePath,
|
|
2626
3597
|
this.config.connector_id,
|
|
2627
|
-
|
|
3598
|
+
this.streamId
|
|
2628
3599
|
);
|
|
2629
3600
|
const metadata = {
|
|
2630
3601
|
sourceFormat: "NEWLINE_DELIMITED_JSON",
|
|
@@ -2633,6 +3604,17 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
|
|
|
2633
3604
|
await this.loadGCSFileInTable(this.sqlStagingTableId, file, metadata);
|
|
2634
3605
|
} catch (err) {
|
|
2635
3606
|
logger.error(`StreamId: ${this.streamId} - Error while uploading stream.`);
|
|
3607
|
+
if (err.code < 500) {
|
|
3608
|
+
await haltAndCatchFire(
|
|
3609
|
+
`unauthorized`,
|
|
3610
|
+
`We couldn't connect to your Cloud Storage bucket \u{1F614}
|
|
3611
|
+
|
|
3612
|
+
The error from Google is: '${err.message}' \u{1F440}
|
|
3613
|
+
|
|
3614
|
+
Could you troubleshoot your Cloud Storage configuration in Google Cloud and sync again the source? \u{1F64F}`,
|
|
3615
|
+
`Got error from cloud storage lib`
|
|
3616
|
+
);
|
|
3617
|
+
}
|
|
2636
3618
|
throw err;
|
|
2637
3619
|
}
|
|
2638
3620
|
};
|
|
@@ -2760,47 +3742,16 @@ var BigQueryTarget = class extends ITarget {
|
|
|
2760
3742
|
convertNumberIntoDecimal = convertNumberIntoDecimal;
|
|
2761
3743
|
};
|
|
2762
3744
|
|
|
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
3745
|
// src/state-providers/gcs/main.ts
|
|
2797
|
-
import { format as
|
|
3746
|
+
import { format as format7 } from "util";
|
|
2798
3747
|
var GCSStateProvider = class {
|
|
2799
3748
|
bucketName;
|
|
2800
3749
|
connectorId;
|
|
3750
|
+
service;
|
|
2801
3751
|
constructor(connectorId, bucketName) {
|
|
2802
3752
|
this.connectorId = connectorId;
|
|
2803
3753
|
this.bucketName = bucketName;
|
|
3754
|
+
this.service = new CloudStorageService(bucketName);
|
|
2804
3755
|
}
|
|
2805
3756
|
getObjectPath() {
|
|
2806
3757
|
return `${this.connectorId}/state.json`;
|
|
@@ -2809,11 +3760,11 @@ var GCSStateProvider = class {
|
|
|
2809
3760
|
const objectPath = this.getObjectPath();
|
|
2810
3761
|
try {
|
|
2811
3762
|
logger.info(`\u{1F4C1} Loading state from gs://${this.bucketName}/${objectPath}`);
|
|
2812
|
-
const raw = await readObjectAsString(
|
|
3763
|
+
const raw = await this.service.readObjectAsString(objectPath);
|
|
2813
3764
|
const parsed = JSON.parse(raw);
|
|
2814
3765
|
return { state: parsed };
|
|
2815
3766
|
} catch (err) {
|
|
2816
|
-
logger.warn(
|
|
3767
|
+
logger.warn(format7(`error loading state from gs://${this.bucketName}/${objectPath}, will start with empty state, err: %s`, err?.message));
|
|
2817
3768
|
return { state: void 0 };
|
|
2818
3769
|
}
|
|
2819
3770
|
}
|
|
@@ -2821,10 +3772,10 @@ var GCSStateProvider = class {
|
|
|
2821
3772
|
const objectPath = this.getObjectPath();
|
|
2822
3773
|
try {
|
|
2823
3774
|
logger.info(`\u{1F4DD} Writing state to gs://${this.bucketName}/${objectPath}`);
|
|
2824
|
-
await writeStringObject(
|
|
3775
|
+
await this.service.writeStringObject(objectPath, state);
|
|
2825
3776
|
} catch (err) {
|
|
2826
|
-
logger.error(
|
|
2827
|
-
throw new Error(
|
|
3777
|
+
logger.error(format7(`\u{1F4A5} Error while writing state to gs://${this.bucketName}/${objectPath}, err: %s`, err?.message));
|
|
3778
|
+
throw new Error(format7(`error writing state to gs://${this.bucketName}/${objectPath}, err: %s`, err?.message));
|
|
2828
3779
|
}
|
|
2829
3780
|
}
|
|
2830
3781
|
};
|
|
@@ -2834,9 +3785,16 @@ export {
|
|
|
2834
3785
|
Authenticator,
|
|
2835
3786
|
BATCH_INTERVAL_MS,
|
|
2836
3787
|
BigQueryTarget,
|
|
3788
|
+
CloudStorageService,
|
|
2837
3789
|
CounterMetric,
|
|
3790
|
+
CsvExtractionConfigBuilder,
|
|
2838
3791
|
DEFAULT_MAX_CONCURRENT_STREAMS,
|
|
2839
3792
|
EXECUTION_TIME_METRIC_NAME,
|
|
3793
|
+
ExcelExtractionConfigBuilder,
|
|
3794
|
+
FileFormat,
|
|
3795
|
+
FilePatterns,
|
|
3796
|
+
FileStream,
|
|
3797
|
+
FileTap,
|
|
2840
3798
|
GCSStateProvider,
|
|
2841
3799
|
ITarget,
|
|
2842
3800
|
MissingFieldInSchemaError,
|
|
@@ -2849,16 +3807,34 @@ export {
|
|
|
2849
3807
|
ReplicationMethodMessage,
|
|
2850
3808
|
SchemaMessage,
|
|
2851
3809
|
SchemaValidationError,
|
|
3810
|
+
default2 as SftpClient,
|
|
3811
|
+
SftpService,
|
|
2852
3812
|
StateMessage,
|
|
2853
3813
|
StateService,
|
|
2854
3814
|
Stream,
|
|
2855
3815
|
StreamWarehouseSyncService,
|
|
2856
3816
|
Tap,
|
|
3817
|
+
VariableExtractors,
|
|
2857
3818
|
_greaterThan,
|
|
2858
3819
|
addWhalyFields,
|
|
3820
|
+
checkCsvHeaderRow,
|
|
3821
|
+
countCsvLines,
|
|
3822
|
+
createCellReference,
|
|
3823
|
+
createCsvStreamConfig,
|
|
3824
|
+
createExcelGenerator,
|
|
3825
|
+
createExcelStreamConfig,
|
|
2859
3826
|
createTemporaryFileStream,
|
|
3827
|
+
csvFieldsToJsonSchema,
|
|
3828
|
+
excelColumnToIndex,
|
|
3829
|
+
excelFieldsToJsonSchema,
|
|
3830
|
+
excelRowToIndex,
|
|
3831
|
+
extractExcelRows,
|
|
3832
|
+
extractPrimaryKeysFromCsvConfig,
|
|
3833
|
+
extractPrimaryKeysFromExcelFields,
|
|
2860
3834
|
extractStateForStream,
|
|
3835
|
+
fieldTypeToJsonSchema,
|
|
2861
3836
|
finalizeStateProgressMarkers,
|
|
3837
|
+
findAllMatchingConfigs,
|
|
2862
3838
|
flattenSchema,
|
|
2863
3839
|
getAllMetrics,
|
|
2864
3840
|
getAxiosInstance,
|
|
@@ -2871,14 +3847,23 @@ export {
|
|
|
2871
3847
|
gracefulExit,
|
|
2872
3848
|
haltAndCatchFire,
|
|
2873
3849
|
incrementStreamState,
|
|
3850
|
+
indexToExcelColumn,
|
|
3851
|
+
indexToExcelRow,
|
|
2874
3852
|
loadJson,
|
|
2875
3853
|
loadSchema,
|
|
2876
3854
|
logger,
|
|
3855
|
+
parseCellReference,
|
|
2877
3856
|
postFormDataApiCall,
|
|
2878
3857
|
postJSONApiCall,
|
|
2879
3858
|
postUrlEncodedApiCall,
|
|
2880
3859
|
printMemoryFootprint,
|
|
3860
|
+
processFileStreams,
|
|
2881
3861
|
removeParasiteProperties,
|
|
2882
|
-
|
|
3862
|
+
rowGeneratorFromCsv,
|
|
3863
|
+
safePath,
|
|
3864
|
+
unzip,
|
|
3865
|
+
validateExcelConfig,
|
|
3866
|
+
writeDataToCsv,
|
|
3867
|
+
writeGeneratorToCSV
|
|
2883
3868
|
};
|
|
2884
3869
|
//# sourceMappingURL=index.mjs.map
|