@whaly/connector-sdk 0.3.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/dist/index.d.mts +17 -5
- package/dist/index.d.ts +17 -5
- package/dist/index.js +73 -87
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +72 -87
- 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.
|
|
@@ -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,21 @@ 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
|
-
return {
|
|
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
|
+
rowData[keyGenerator.key] = keyGenerator.valueTransformer ? keyGenerator.valueTransformer(row[key]) : row[key];
|
|
2537
|
+
}
|
|
2523
2538
|
yield rowData;
|
|
2524
2539
|
} else {
|
|
2525
|
-
const
|
|
2540
|
+
const rowKeys = Object.keys(row);
|
|
2541
|
+
const rowData = fileConfig.addSyncedAtColumn ? { _wly_synced_at: syncedAt } : {};
|
|
2542
|
+
for (let i = 0; i < rowKeys.length; i++) {
|
|
2526
2543
|
const keyGenerator = fileConfig.fields[i];
|
|
2527
|
-
if (!keyGenerator)
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
return {
|
|
2531
|
-
...acc,
|
|
2532
|
-
[keyGenerator.key]: keyGenerator.valueTransformer ? keyGenerator.valueTransformer(row[i]) : row[i]
|
|
2533
|
-
};
|
|
2534
|
-
}, fileConfig.addSyncedAtColumn ? { _wly_synced_at: syncedAt } : {});
|
|
2544
|
+
if (!keyGenerator) continue;
|
|
2545
|
+
rowData[keyGenerator.key] = keyGenerator.valueTransformer ? keyGenerator.valueTransformer(row[i]) : row[i];
|
|
2546
|
+
}
|
|
2535
2547
|
yield rowData;
|
|
2536
2548
|
}
|
|
2537
2549
|
}
|
|
@@ -2539,6 +2551,17 @@ async function* rowGeneratorFromCsv(path2, fileConfig) {
|
|
|
2539
2551
|
initialReadStream.destroy();
|
|
2540
2552
|
}
|
|
2541
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
|
+
}
|
|
2542
2565
|
var checkCsvHeaderRow = async (path2, fileConfig) => {
|
|
2543
2566
|
return new Promise((resolve, reject) => {
|
|
2544
2567
|
const colsWithParsingConfig = Object.keys(fileConfig.fields).map((key) => key.trim());
|
|
@@ -2732,8 +2755,19 @@ var FileStream = class extends Stream {
|
|
|
2732
2755
|
/**
|
|
2733
2756
|
* Yield records from all file(s).
|
|
2734
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.
|
|
2735
2759
|
*/
|
|
2736
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;
|
|
2737
2771
|
for (const filePath of this.localFilePaths) {
|
|
2738
2772
|
if (this.config.format === "EXCEL" /* EXCEL */) {
|
|
2739
2773
|
const data = await extractExcelRows(filePath, this.config.excel);
|
|
@@ -2982,60 +3016,7 @@ import { Storage } from "@google-cloud/storage";
|
|
|
2982
3016
|
import { format as format6 } from "util";
|
|
2983
3017
|
import * as pathModule from "path";
|
|
2984
3018
|
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
|
|
3019
|
+
import { randomUUID } from "crypto";
|
|
3039
3020
|
var logPrefix3 = "[CloudStorageService]";
|
|
3040
3021
|
var tmpDir = "tmp";
|
|
3041
3022
|
var CloudStorageService = class {
|
|
@@ -3173,12 +3154,15 @@ var CloudStorageService = class {
|
|
|
3173
3154
|
}
|
|
3174
3155
|
/**
|
|
3175
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.
|
|
3176
3159
|
* Returns the GCS File reference.
|
|
3177
3160
|
*/
|
|
3178
3161
|
async uploadFileWithUniqueName(filePath, prefix, streamId) {
|
|
3179
3162
|
try {
|
|
3180
3163
|
await this.bucket.get({ autoCreate: false });
|
|
3181
|
-
const
|
|
3164
|
+
const runFolder = process.env.RUN_ID ?? "default";
|
|
3165
|
+
const destinationFileName = `${prefix}/${runFolder}/${streamId}-${randomUUID()}.jsonnl`;
|
|
3182
3166
|
await this.bucket.upload(filePath, {
|
|
3183
3167
|
destination: destinationFileName
|
|
3184
3168
|
});
|
|
@@ -3834,6 +3818,7 @@ export {
|
|
|
3834
3818
|
_greaterThan,
|
|
3835
3819
|
addWhalyFields,
|
|
3836
3820
|
checkCsvHeaderRow,
|
|
3821
|
+
countCsvLines,
|
|
3837
3822
|
createCellReference,
|
|
3838
3823
|
createCsvStreamConfig,
|
|
3839
3824
|
createExcelGenerator,
|