@whaly/connector-sdk 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +19 -7
- package/dist/index.d.ts +19 -7
- package/dist/index.js +77 -89
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +76 -89
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/dist/index.mjs
CHANGED
|
@@ -728,6 +728,8 @@ var Stream = class {
|
|
|
728
728
|
// However, a STATE will still be emitted if the Stream is using an incremental sync type.
|
|
729
729
|
isSilent = false;
|
|
730
730
|
childConcurrency;
|
|
731
|
+
// Optional total row count for percentage progress logging
|
|
732
|
+
totalRows;
|
|
731
733
|
// Metrics configuration
|
|
732
734
|
rowsSyncedMetricsConf;
|
|
733
735
|
executionTimeMetricsConf;
|
|
@@ -1005,6 +1007,10 @@ var Stream = class {
|
|
|
1005
1007
|
metric.increment();
|
|
1006
1008
|
});
|
|
1007
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
|
+
}
|
|
1008
1014
|
}
|
|
1009
1015
|
const stateServiceInst = StateService.getInstance();
|
|
1010
1016
|
const state = stateServiceInst.getBookmark(this.streamId);
|
|
@@ -1677,7 +1683,8 @@ var flattenSchema = (streamId, schema) => {
|
|
|
1677
1683
|
};
|
|
1678
1684
|
|
|
1679
1685
|
// src/sdk/models/target/target.ts
|
|
1680
|
-
import
|
|
1686
|
+
import Ajv from "ajv";
|
|
1687
|
+
import addFormats from "ajv-formats";
|
|
1681
1688
|
import dayjs4 from "dayjs";
|
|
1682
1689
|
|
|
1683
1690
|
// src/sdk/models/target/streamDbState.ts
|
|
@@ -1755,6 +1762,8 @@ var StreamState = class {
|
|
|
1755
1762
|
// So we need to keep track of whether we are at the first load or not
|
|
1756
1763
|
_hasBeenLoadedYetDuringThisSync;
|
|
1757
1764
|
replicationMethod;
|
|
1765
|
+
// Pre-compiled ajv ValidateFunction — compiled once per schema, reused for every row
|
|
1766
|
+
compiledValidateFn;
|
|
1758
1767
|
constructor(streamId, dbSync, replicationMethod) {
|
|
1759
1768
|
this.streamId = streamId;
|
|
1760
1769
|
this.dbSync = dbSync;
|
|
@@ -1772,6 +1781,12 @@ var StreamState = class {
|
|
|
1772
1781
|
getSchema() {
|
|
1773
1782
|
return this.schema;
|
|
1774
1783
|
}
|
|
1784
|
+
setCompiledValidateFn(fn) {
|
|
1785
|
+
this.compiledValidateFn = fn;
|
|
1786
|
+
}
|
|
1787
|
+
getCompiledValidateFn() {
|
|
1788
|
+
return this.compiledValidateFn;
|
|
1789
|
+
}
|
|
1775
1790
|
getBatchDate() {
|
|
1776
1791
|
return this.batchDate.format(defaultDateTimeFormat);
|
|
1777
1792
|
}
|
|
@@ -1828,8 +1843,8 @@ var ITarget = class _ITarget {
|
|
|
1828
1843
|
flushedState;
|
|
1829
1844
|
renameColumnStore;
|
|
1830
1845
|
streams;
|
|
1831
|
-
// JSON Schema validator
|
|
1832
|
-
|
|
1846
|
+
// JSON Schema validator (ajv — pre-compiles schemas into optimized JS functions)
|
|
1847
|
+
ajv;
|
|
1833
1848
|
constructor(config, stateProvider) {
|
|
1834
1849
|
this.config = config;
|
|
1835
1850
|
this.stateProvider = stateProvider;
|
|
@@ -1837,7 +1852,7 @@ var ITarget = class _ITarget {
|
|
|
1837
1852
|
this.streams = {};
|
|
1838
1853
|
this.batchedState = {};
|
|
1839
1854
|
this.flushedState = {};
|
|
1840
|
-
this.
|
|
1855
|
+
this.ajv = addFormats(new Ajv({ allErrors: true, strict: false }));
|
|
1841
1856
|
this.syncTime = dayjs4();
|
|
1842
1857
|
this.renameColumnStore = new RenameColumnStore();
|
|
1843
1858
|
}
|
|
@@ -1896,12 +1911,14 @@ var ITarget = class _ITarget {
|
|
|
1896
1911
|
if (!stream) {
|
|
1897
1912
|
throw new MissingSchemaError(`A record for stream:\`${streamId}\` was encountered before a corresponding schema.`, streamId);
|
|
1898
1913
|
}
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
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
|
+
}
|
|
1905
1922
|
const record = message.record;
|
|
1906
1923
|
const schema = stream.getSchema();
|
|
1907
1924
|
if (!schema) {
|
|
@@ -1910,13 +1927,13 @@ var ITarget = class _ITarget {
|
|
|
1910
1927
|
}
|
|
1911
1928
|
const recordWithoutParasiteProperties = removeParasiteProperties(record, schema);
|
|
1912
1929
|
const recordWithWhalyFields = addWhalyFields(recordWithoutParasiteProperties, stream.getBatchDate());
|
|
1913
|
-
const
|
|
1914
|
-
if (!
|
|
1930
|
+
const validateFn = stream.getCompiledValidateFn();
|
|
1931
|
+
if (validateFn && !validateFn(recordWithWhalyFields)) {
|
|
1915
1932
|
throw new SchemaValidationError(`Stream: ${streamId} - Record is not valid according to schema.
|
|
1916
|
-
Validation errors: ${JSON.stringify(
|
|
1933
|
+
Validation errors: ${JSON.stringify(validateFn.errors)}
|
|
1917
1934
|
|
|
1918
1935
|
Record: ${JSON.stringify(recordWithoutParasiteProperties)}
|
|
1919
|
-
Schema: ${JSON.stringify(stream.getSchema())}`, streamId,
|
|
1936
|
+
Schema: ${JSON.stringify(stream.getSchema())}`, streamId, validateFn.errors ?? []);
|
|
1920
1937
|
}
|
|
1921
1938
|
const recordWithValidDate = this.validateDateRange(recordWithWhalyFields, schema);
|
|
1922
1939
|
const recordWithDecimal = this.convertNumberIntoDecimal(recordWithValidDate, schema);
|
|
@@ -2005,6 +2022,7 @@ var ITarget = class _ITarget {
|
|
|
2005
2022
|
}
|
|
2006
2023
|
});
|
|
2007
2024
|
streamState.setSchema(schema);
|
|
2025
|
+
streamState.setCompiledValidateFn(this.ajv.compile({ type: "object", properties: schema }));
|
|
2008
2026
|
const streamReplicationMethod = this.streams[streamId]?.getReplicationMethod();
|
|
2009
2027
|
if (message.keyProperties.length === 0 && streamReplicationMethod !== "APPEND" /* APPEND */) {
|
|
2010
2028
|
throw new Error(`StreamId: ${message.stream} - \`key_properties\` field is required and can't be empty in SCHEMA message.
|
|
@@ -2287,10 +2305,10 @@ var extractSingleSheetRows = async (fileName, conf) => {
|
|
|
2287
2305
|
if (!variables) {
|
|
2288
2306
|
throw new Error(`No variables extracted from filename, cannot extract derived field ${key}`);
|
|
2289
2307
|
}
|
|
2290
|
-
acc[key] = variables[column.variableName] ||
|
|
2308
|
+
acc[key] = variables[column.variableName] || null;
|
|
2291
2309
|
} else {
|
|
2292
2310
|
const colIndex = excelColumnToIndex(column.column);
|
|
2293
|
-
acc[key] = row[colIndex]?.v ||
|
|
2311
|
+
acc[key] = row[colIndex]?.v || null;
|
|
2294
2312
|
}
|
|
2295
2313
|
return acc;
|
|
2296
2314
|
}, {});
|
|
@@ -2490,6 +2508,7 @@ Actual: "${actual}" (${actualInfo.length} chars) [${actualInfo.hex}]`;
|
|
|
2490
2508
|
};
|
|
2491
2509
|
async function* rowGeneratorFromCsv(path2, fileConfig) {
|
|
2492
2510
|
const isGeneratorConfigArray = Array.isArray(fileConfig.fields);
|
|
2511
|
+
const configKeys = isGeneratorConfigArray ? [] : Object.keys(fileConfig.fields);
|
|
2493
2512
|
const csvOptions = { separator: fileConfig.separator };
|
|
2494
2513
|
if (isGeneratorConfigArray) {
|
|
2495
2514
|
csvOptions.headers = false;
|
|
@@ -2510,28 +2529,23 @@ async function* rowGeneratorFromCsv(path2, fileConfig) {
|
|
|
2510
2529
|
try {
|
|
2511
2530
|
for await (const row of Readable.from(csvStream)) {
|
|
2512
2531
|
if (!isGeneratorConfigArray) {
|
|
2513
|
-
const rowData =
|
|
2532
|
+
const rowData = fileConfig.addSyncedAtColumn ? { _wly_synced_at: syncedAt } : {};
|
|
2533
|
+
for (const key of configKeys) {
|
|
2514
2534
|
const keyGenerator = fileConfig.fields[key.trim()];
|
|
2515
|
-
if (!keyGenerator)
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
...acc,
|
|
2520
|
-
[keyGenerator.key]: keyGenerator.valueTransformer ? keyGenerator.valueTransformer(row[key]) : row[key]
|
|
2521
|
-
};
|
|
2522
|
-
}, fileConfig.addSyncedAtColumn ? { _wly_synced_at: syncedAt } : {});
|
|
2535
|
+
if (!keyGenerator) continue;
|
|
2536
|
+
const rawValue = keyGenerator.valueTransformer ? keyGenerator.valueTransformer(row[key]) : row[key];
|
|
2537
|
+
rowData[keyGenerator.key] = rawValue === "" ? null : rawValue;
|
|
2538
|
+
}
|
|
2523
2539
|
yield rowData;
|
|
2524
2540
|
} else {
|
|
2525
|
-
const
|
|
2541
|
+
const rowKeys = Object.keys(row);
|
|
2542
|
+
const rowData = fileConfig.addSyncedAtColumn ? { _wly_synced_at: syncedAt } : {};
|
|
2543
|
+
for (let i = 0; i < rowKeys.length; i++) {
|
|
2526
2544
|
const keyGenerator = fileConfig.fields[i];
|
|
2527
|
-
if (!keyGenerator)
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
...acc,
|
|
2532
|
-
[keyGenerator.key]: keyGenerator.valueTransformer ? keyGenerator.valueTransformer(row[i]) : row[i]
|
|
2533
|
-
};
|
|
2534
|
-
}, fileConfig.addSyncedAtColumn ? { _wly_synced_at: syncedAt } : {});
|
|
2545
|
+
if (!keyGenerator) continue;
|
|
2546
|
+
const rawValue = keyGenerator.valueTransformer ? keyGenerator.valueTransformer(row[i]) : row[i];
|
|
2547
|
+
rowData[keyGenerator.key] = rawValue === "" ? null : rawValue;
|
|
2548
|
+
}
|
|
2535
2549
|
yield rowData;
|
|
2536
2550
|
}
|
|
2537
2551
|
}
|
|
@@ -2539,6 +2553,17 @@ async function* rowGeneratorFromCsv(path2, fileConfig) {
|
|
|
2539
2553
|
initialReadStream.destroy();
|
|
2540
2554
|
}
|
|
2541
2555
|
}
|
|
2556
|
+
async function countCsvLines(filePath) {
|
|
2557
|
+
return new Promise((resolve, reject) => {
|
|
2558
|
+
let newlines = 0;
|
|
2559
|
+
createReadStream(filePath).on("data", (chunk2) => {
|
|
2560
|
+
const buf = Buffer.isBuffer(chunk2) ? chunk2 : Buffer.from(chunk2);
|
|
2561
|
+
for (let i = 0; i < buf.length; i++) {
|
|
2562
|
+
if (buf[i] === 10) newlines++;
|
|
2563
|
+
}
|
|
2564
|
+
}).on("end", () => resolve(Math.max(0, newlines - 1))).on("error", reject);
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2542
2567
|
var checkCsvHeaderRow = async (path2, fileConfig) => {
|
|
2543
2568
|
return new Promise((resolve, reject) => {
|
|
2544
2569
|
const colsWithParsingConfig = Object.keys(fileConfig.fields).map((key) => key.trim());
|
|
@@ -2732,8 +2757,19 @@ var FileStream = class extends Stream {
|
|
|
2732
2757
|
/**
|
|
2733
2758
|
* Yield records from all file(s).
|
|
2734
2759
|
* When multiple file paths are provided, records are yielded sequentially from each file.
|
|
2760
|
+
* Pre-counts total rows across all files to enable percentage progress logging.
|
|
2735
2761
|
*/
|
|
2736
2762
|
async *_getRecords() {
|
|
2763
|
+
let total = 0;
|
|
2764
|
+
for (const filePath of this.localFilePaths) {
|
|
2765
|
+
if (this.config.format === "EXCEL" /* EXCEL */) {
|
|
2766
|
+
const data = await extractExcelRows(filePath, this.config.excel);
|
|
2767
|
+
total += data.length;
|
|
2768
|
+
} else if (this.config.format === "CSV" /* CSV */) {
|
|
2769
|
+
total += await countCsvLines(filePath);
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
this.totalRows = total;
|
|
2737
2773
|
for (const filePath of this.localFilePaths) {
|
|
2738
2774
|
if (this.config.format === "EXCEL" /* EXCEL */) {
|
|
2739
2775
|
const data = await extractExcelRows(filePath, this.config.excel);
|
|
@@ -2982,60 +3018,7 @@ import { Storage } from "@google-cloud/storage";
|
|
|
2982
3018
|
import { format as format6 } from "util";
|
|
2983
3019
|
import * as pathModule from "path";
|
|
2984
3020
|
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
|
|
3021
|
+
import { randomUUID } from "crypto";
|
|
3039
3022
|
var logPrefix3 = "[CloudStorageService]";
|
|
3040
3023
|
var tmpDir = "tmp";
|
|
3041
3024
|
var CloudStorageService = class {
|
|
@@ -3173,12 +3156,15 @@ var CloudStorageService = class {
|
|
|
3173
3156
|
}
|
|
3174
3157
|
/**
|
|
3175
3158
|
* Uploads a local file to the bucket with a unique name based on prefix, streamId, and UUID.
|
|
3159
|
+
* Files are stored under `<prefix>/<run-id>/` when the RUN_ID env var is set,
|
|
3160
|
+
* or `<prefix>/default/` otherwise.
|
|
3176
3161
|
* Returns the GCS File reference.
|
|
3177
3162
|
*/
|
|
3178
3163
|
async uploadFileWithUniqueName(filePath, prefix, streamId) {
|
|
3179
3164
|
try {
|
|
3180
3165
|
await this.bucket.get({ autoCreate: false });
|
|
3181
|
-
const
|
|
3166
|
+
const runFolder = process.env.RUN_ID ?? "default";
|
|
3167
|
+
const destinationFileName = `${prefix}/${runFolder}/${streamId}-${randomUUID()}.jsonnl`;
|
|
3182
3168
|
await this.bucket.upload(filePath, {
|
|
3183
3169
|
destination: destinationFileName
|
|
3184
3170
|
});
|
|
@@ -3834,6 +3820,7 @@ export {
|
|
|
3834
3820
|
_greaterThan,
|
|
3835
3821
|
addWhalyFields,
|
|
3836
3822
|
checkCsvHeaderRow,
|
|
3823
|
+
countCsvLines,
|
|
3837
3824
|
createCellReference,
|
|
3838
3825
|
createCsvStreamConfig,
|
|
3839
3826
|
createExcelGenerator,
|