@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.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 { Validator } from "jsonschema";
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
- validator;
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.validator = new Validator();
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
- await semaphore.runExclusive(async () => {
1900
- const shouldUploadIntermediateBatch = this.shouldUploadIntermediateBatch(streamId);
1901
- if (shouldUploadIntermediateBatch) {
1902
- await this.loadAllStreamsInWarehouse({ isFinalLoad: false });
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 validationResult = this.validator.validate(recordWithWhalyFields, schema);
1914
- if (!validationResult.valid) {
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(validationResult.errors)}
1933
+ Validation errors: ${JSON.stringify(validateFn.errors)}
1917
1934
 
1918
1935
  Record: ${JSON.stringify(recordWithoutParasiteProperties)}
1919
- Schema: ${JSON.stringify(stream.getSchema())}`, streamId, validationResult.errors);
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 = Object.keys(fileConfig.fields).reduce((acc, key) => {
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
- return acc;
2517
- }
2518
- return {
2519
- ...acc,
2520
- [keyGenerator.key]: keyGenerator.valueTransformer ? keyGenerator.valueTransformer(row[key]) : row[key]
2521
- };
2522
- }, fileConfig.addSyncedAtColumn ? { _wly_synced_at: syncedAt } : {});
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 rowData = Object.keys(row).reduce((acc, key, i) => {
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
- return acc;
2529
- }
2530
- return {
2531
- ...acc,
2532
- [keyGenerator.key]: keyGenerator.valueTransformer ? keyGenerator.valueTransformer(row[i]) : row[i]
2533
- };
2534
- }, fileConfig.addSyncedAtColumn ? { _wly_synced_at: syncedAt } : {});
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 destinationFileName = `${prefix}/${streamId}-${v4_default()}.jsonnl`;
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,