@whaly/connector-sdk 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -0
- package/dist/index.d.mts +568 -200
- package/dist/index.d.ts +568 -200
- package/dist/index.js +1389 -584
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1354 -579
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -5
package/dist/index.mjs
CHANGED
|
@@ -2,180 +2,10 @@
|
|
|
2
2
|
import { readFileSync } from "fs";
|
|
3
3
|
|
|
4
4
|
// src/sdk/service/logger.ts
|
|
5
|
-
import * as
|
|
6
|
-
|
|
7
|
-
// src/sdk/models/resolver.ts
|
|
8
|
-
var Resolver = class {
|
|
9
|
-
constructor() {
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
// src/resolvers/local-file/services/files.ts
|
|
14
|
-
import fs from "fs";
|
|
15
|
-
var writeFile = (path, content) => {
|
|
16
|
-
return Promise.resolve(fs.writeFileSync(path, content));
|
|
17
|
-
};
|
|
18
|
-
var readAndParseJSONFile = (path) => {
|
|
19
|
-
if (!fs.existsSync(path)) {
|
|
20
|
-
throw new Error(`File \`${path}\` wasn't found at: ${process.cwd()}`);
|
|
21
|
-
}
|
|
22
|
-
const raw = fs.readFileSync(path);
|
|
23
|
-
const parsed = JSON.parse(raw.toString());
|
|
24
|
-
return parsed;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
// src/resolvers/local-file/hook/schemaHook.ts
|
|
28
|
-
import fsExtra from "fs-extra";
|
|
29
|
-
var LocalSchemaHook = class {
|
|
30
|
-
async writeSchema(input) {
|
|
31
|
-
await fsExtra.appendFile("./schema.tmp", JSON.stringify(input) + "\n");
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
// src/resolvers/local-file/resolver.ts
|
|
36
|
-
import winston from "winston";
|
|
37
|
-
|
|
38
|
-
// src/sdk/service/metric.ts
|
|
39
|
-
var metricStore = {};
|
|
40
|
-
var ALL_STREAM_ID_KEYWORD = "all";
|
|
41
|
-
var ROWS_SYNCED_METRIC_NAME = "rows_synced_count";
|
|
42
|
-
var API_CALLS_METRIC_NAME = "api_calls_count";
|
|
43
|
-
var EXECUTION_TIME_METRIC_NAME = "execution_time_ms";
|
|
44
|
-
var getCounterMetrics = (name, streamIds) => {
|
|
45
|
-
return streamIds.map((streamId) => {
|
|
46
|
-
return getCounterMetric(name, streamId);
|
|
47
|
-
});
|
|
48
|
-
};
|
|
49
|
-
var getCounterMetric = (name, streamId = ALL_STREAM_ID_KEYWORD) => {
|
|
50
|
-
if (!metricStore[streamId]) {
|
|
51
|
-
metricStore[streamId] = {};
|
|
52
|
-
}
|
|
53
|
-
const streamMetricStore = metricStore[streamId];
|
|
54
|
-
if (!streamMetricStore[name]) {
|
|
55
|
-
streamMetricStore[name] = new CounterMetric(name, streamId);
|
|
56
|
-
}
|
|
57
|
-
return streamMetricStore[name];
|
|
58
|
-
};
|
|
59
|
-
var getAllMetrics = () => {
|
|
60
|
-
return Object.keys(metricStore).flatMap((streamId) => {
|
|
61
|
-
const streamMetricStore = metricStore[streamId];
|
|
62
|
-
if (!streamMetricStore) {
|
|
63
|
-
return [];
|
|
64
|
-
}
|
|
65
|
-
return Object.keys(streamMetricStore).map((metricName) => streamMetricStore[metricName]).filter((m) => m !== void 0);
|
|
66
|
-
});
|
|
67
|
-
};
|
|
68
|
-
var CounterMetric = class {
|
|
69
|
-
value = 0;
|
|
70
|
-
name;
|
|
71
|
-
streamId;
|
|
72
|
-
constructor(name, streamId) {
|
|
73
|
-
this.name = name;
|
|
74
|
-
this.streamId = streamId;
|
|
75
|
-
}
|
|
76
|
-
increment(inc = 1) {
|
|
77
|
-
this.value = this.value + inc;
|
|
78
|
-
}
|
|
79
|
-
getStreamId() {
|
|
80
|
-
return this.streamId;
|
|
81
|
-
}
|
|
82
|
-
getName() {
|
|
83
|
-
return this.name;
|
|
84
|
-
}
|
|
85
|
-
getValue() {
|
|
86
|
-
return this.value;
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// src/resolvers/local-file/resolver.ts
|
|
91
|
-
var { combine, prettyPrint, timestamp, errors, splat } = winston.format;
|
|
92
|
-
var statePath = "./state.json";
|
|
93
|
-
var metricsPath = "./metrics.json";
|
|
94
|
-
var LocalFilesResolver = class extends Resolver {
|
|
95
|
-
checkIfCanSync() {
|
|
96
|
-
return Promise.resolve(true);
|
|
97
|
-
}
|
|
98
|
-
markSyncStarted() {
|
|
99
|
-
return Promise.resolve();
|
|
100
|
-
}
|
|
101
|
-
onError(errorType, errorText, errorDebugText) {
|
|
102
|
-
const path = `./error.json`;
|
|
103
|
-
return writeFile(path, JSON.stringify({
|
|
104
|
-
errorType,
|
|
105
|
-
errorText,
|
|
106
|
-
errorDebugText
|
|
107
|
-
}));
|
|
108
|
-
}
|
|
109
|
-
getSchemaHooks() {
|
|
110
|
-
const wlyHook = new LocalSchemaHook();
|
|
111
|
-
return [wlyHook];
|
|
112
|
-
}
|
|
113
|
-
writeSchema() {
|
|
114
|
-
return Promise.resolve();
|
|
115
|
-
}
|
|
116
|
-
constructor() {
|
|
117
|
-
super();
|
|
118
|
-
}
|
|
119
|
-
getState() {
|
|
120
|
-
logger.info(`\u{1F4C1} Using the LOCAL resolver`);
|
|
121
|
-
const state = readAndParseJSONFile(statePath);
|
|
122
|
-
return Promise.resolve({
|
|
123
|
-
state
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
writeCatalog(catalog) {
|
|
127
|
-
return Promise.resolve();
|
|
128
|
-
}
|
|
129
|
-
writeState(state) {
|
|
130
|
-
return writeFile(statePath, state);
|
|
131
|
-
}
|
|
132
|
-
markSyncFailed() {
|
|
133
|
-
logger.info(`\u{1F622} Sync has failed.`);
|
|
134
|
-
return Promise.resolve();
|
|
135
|
-
}
|
|
136
|
-
markSyncComplete() {
|
|
137
|
-
logger.info(`\u{1F389} Sync is complete.`);
|
|
138
|
-
return Promise.resolve();
|
|
139
|
-
}
|
|
140
|
-
markDiscoveryComplete() {
|
|
141
|
-
logger.info(`\u{1F389} Discovery is complete.`);
|
|
142
|
-
return Promise.resolve();
|
|
143
|
-
}
|
|
144
|
-
async flushMetrics() {
|
|
145
|
-
const allMetrics = getAllMetrics();
|
|
146
|
-
const metricsOuput = allMetrics.map((metric) => {
|
|
147
|
-
return {
|
|
148
|
-
streamId: metric.getStreamId(),
|
|
149
|
-
name: metric.getName(),
|
|
150
|
-
value: metric.getValue()
|
|
151
|
-
};
|
|
152
|
-
}).map((obj) => JSON.stringify(obj)).join(`
|
|
153
|
-
`);
|
|
154
|
-
return writeFile(metricsPath, metricsOuput);
|
|
155
|
-
}
|
|
156
|
-
updateSourceValue(optionKey, optionValue) {
|
|
157
|
-
logger.info(`Updating source value ${optionKey} with value ${optionValue}`, { private: true });
|
|
158
|
-
return Promise.resolve();
|
|
159
|
-
}
|
|
160
|
-
getLogFormat() {
|
|
161
|
-
return combine(
|
|
162
|
-
timestamp(),
|
|
163
|
-
errors({ stack: true }),
|
|
164
|
-
splat(),
|
|
165
|
-
prettyPrint()
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
// src/sdk/service/resolver.ts
|
|
171
|
-
var loadResolver = () => {
|
|
172
|
-
return new LocalFilesResolver();
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
// src/sdk/service/logger.ts
|
|
5
|
+
import * as winston from "winston";
|
|
176
6
|
var BATCH_INTERVAL_MS = 100;
|
|
177
7
|
var transports2 = {
|
|
178
|
-
console: new
|
|
8
|
+
console: new winston.transports.Console({
|
|
179
9
|
stderrLevels: [],
|
|
180
10
|
level: "info"
|
|
181
11
|
})
|
|
@@ -185,8 +15,14 @@ var getTransports = () => {
|
|
|
185
15
|
transports2.console
|
|
186
16
|
];
|
|
187
17
|
};
|
|
188
|
-
var
|
|
189
|
-
|
|
18
|
+
var { combine, prettyPrint, timestamp, errors, splat } = winston.format;
|
|
19
|
+
var winstonLogger = winston.createLogger({
|
|
20
|
+
format: combine(
|
|
21
|
+
timestamp(),
|
|
22
|
+
errors({ stack: true }),
|
|
23
|
+
splat(),
|
|
24
|
+
prettyPrint()
|
|
25
|
+
),
|
|
190
26
|
transports: getTransports()
|
|
191
27
|
});
|
|
192
28
|
var isWinstonOpen = true;
|
|
@@ -215,10 +51,6 @@ var logger = {
|
|
|
215
51
|
};
|
|
216
52
|
|
|
217
53
|
// src/sdk/utils.ts
|
|
218
|
-
var writeStateFile = (resolver, state) => {
|
|
219
|
-
logger.info(`\u{1F4DD} Writing the state file.`);
|
|
220
|
-
return resolver.writeState(state);
|
|
221
|
-
};
|
|
222
54
|
function readFile(fileName, filePath) {
|
|
223
55
|
try {
|
|
224
56
|
return readFileSync(filePath);
|
|
@@ -248,7 +80,7 @@ var loadSchema = (streamId) => {
|
|
|
248
80
|
// src/sdk/service/network.ts
|
|
249
81
|
import axios from "axios";
|
|
250
82
|
import { URLSearchParams } from "url";
|
|
251
|
-
import * as
|
|
83
|
+
import * as fs from "fs";
|
|
252
84
|
import * as qs from "qs";
|
|
253
85
|
import axiosRetry, { isNetworkOrIdempotentRequestError } from "axios-retry";
|
|
254
86
|
var shouldRetry = (error) => {
|
|
@@ -361,7 +193,7 @@ var getJSONApiCall = async (endpoint, config, privateLogging) => {
|
|
|
361
193
|
};
|
|
362
194
|
var getDownloadFileApiCall = async (endpoint, config, output, privateLogging) => {
|
|
363
195
|
const instance = getAxiosInstance(config.retryCount);
|
|
364
|
-
const writer =
|
|
196
|
+
const writer = fs.createWriteStream(output);
|
|
365
197
|
const paramsSerializer = (params) => {
|
|
366
198
|
return qs.stringify(params, { arrayFormat: "repeat" });
|
|
367
199
|
};
|
|
@@ -455,6 +287,58 @@ var postUrlEncodedApiCall = async (endpoint, config, payload) => {
|
|
|
455
287
|
}
|
|
456
288
|
};
|
|
457
289
|
|
|
290
|
+
// src/sdk/service/metric.ts
|
|
291
|
+
var metricStore = {};
|
|
292
|
+
var ALL_STREAM_ID_KEYWORD = "all";
|
|
293
|
+
var ROWS_SYNCED_METRIC_NAME = "rows_synced_count";
|
|
294
|
+
var API_CALLS_METRIC_NAME = "api_calls_count";
|
|
295
|
+
var EXECUTION_TIME_METRIC_NAME = "execution_time_ms";
|
|
296
|
+
var getCounterMetrics = (name, streamIds) => {
|
|
297
|
+
return streamIds.map((streamId) => {
|
|
298
|
+
return getCounterMetric(name, streamId);
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
var getCounterMetric = (name, streamId = ALL_STREAM_ID_KEYWORD) => {
|
|
302
|
+
if (!metricStore[streamId]) {
|
|
303
|
+
metricStore[streamId] = {};
|
|
304
|
+
}
|
|
305
|
+
const streamMetricStore = metricStore[streamId];
|
|
306
|
+
if (!streamMetricStore[name]) {
|
|
307
|
+
streamMetricStore[name] = new CounterMetric(name, streamId);
|
|
308
|
+
}
|
|
309
|
+
return streamMetricStore[name];
|
|
310
|
+
};
|
|
311
|
+
var getAllMetrics = () => {
|
|
312
|
+
return Object.keys(metricStore).flatMap((streamId) => {
|
|
313
|
+
const streamMetricStore = metricStore[streamId];
|
|
314
|
+
if (!streamMetricStore) {
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
return Object.keys(streamMetricStore).map((metricName) => streamMetricStore[metricName]).filter((m) => m !== void 0);
|
|
318
|
+
});
|
|
319
|
+
};
|
|
320
|
+
var CounterMetric = class {
|
|
321
|
+
value = 0;
|
|
322
|
+
name;
|
|
323
|
+
streamId;
|
|
324
|
+
constructor(name, streamId) {
|
|
325
|
+
this.name = name;
|
|
326
|
+
this.streamId = streamId;
|
|
327
|
+
}
|
|
328
|
+
increment(inc = 1) {
|
|
329
|
+
this.value = this.value + inc;
|
|
330
|
+
}
|
|
331
|
+
getStreamId() {
|
|
332
|
+
return this.streamId;
|
|
333
|
+
}
|
|
334
|
+
getName() {
|
|
335
|
+
return this.name;
|
|
336
|
+
}
|
|
337
|
+
getValue() {
|
|
338
|
+
return this.value;
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
458
342
|
// src/sdk/service/memory.ts
|
|
459
343
|
var formatMemoryUsage = (data) => `${Math.round(data / 1024 / 1024 * 100) / 100} MB`;
|
|
460
344
|
var printMemoryFootprint = (prefix) => {
|
|
@@ -470,9 +354,6 @@ var printMemoryFootprint = (prefix) => {
|
|
|
470
354
|
|
|
471
355
|
// src/sdk/service/exit.ts
|
|
472
356
|
async function gracefulExit(exitCode) {
|
|
473
|
-
if (exitCode !== 0) {
|
|
474
|
-
await loadResolver().markSyncFailed();
|
|
475
|
-
}
|
|
476
357
|
const loggerFinish = new Promise((resolve, reject) => {
|
|
477
358
|
logger.closeWinston(resolve);
|
|
478
359
|
});
|
|
@@ -492,34 +373,16 @@ var haltAndCatchFire = async (errorType, errorText, errorDebugText) => {
|
|
|
492
373
|
errorText,
|
|
493
374
|
errorDebugText
|
|
494
375
|
);
|
|
495
|
-
const resolver = loadResolver();
|
|
496
|
-
await resolver.onError(errorType, errorText, errorDebugText);
|
|
497
376
|
await gracefulExit(0);
|
|
498
377
|
};
|
|
499
378
|
|
|
500
|
-
// src/sdk/
|
|
501
|
-
var
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
};
|
|
508
|
-
if (rel.left === streamId) {
|
|
509
|
-
acc.right.push({
|
|
510
|
-
streamId: rel.right,
|
|
511
|
-
...base
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
if (rel.right === streamId) {
|
|
515
|
-
acc.left.push({
|
|
516
|
-
streamId: rel.left,
|
|
517
|
-
...base
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
return acc;
|
|
521
|
-
}, { left: [], right: [] });
|
|
522
|
-
};
|
|
379
|
+
// src/sdk/models/replication.ts
|
|
380
|
+
var ReplicationMethod = /* @__PURE__ */ ((ReplicationMethod2) => {
|
|
381
|
+
ReplicationMethod2["INCREMENTAL"] = "INCREMENTAL";
|
|
382
|
+
ReplicationMethod2["FULL_TABLE"] = "FULL_TABLE";
|
|
383
|
+
ReplicationMethod2["APPEND"] = "APPEND";
|
|
384
|
+
return ReplicationMethod2;
|
|
385
|
+
})(ReplicationMethod || {});
|
|
523
386
|
|
|
524
387
|
// src/sdk/models/messages.ts
|
|
525
388
|
var RecordMessage = class {
|
|
@@ -579,7 +442,6 @@ var SchemaMessage = class {
|
|
|
579
442
|
displayLabel;
|
|
580
443
|
description;
|
|
581
444
|
propertiesMetadata;
|
|
582
|
-
relationships;
|
|
583
445
|
constructor(opts) {
|
|
584
446
|
const {
|
|
585
447
|
stream,
|
|
@@ -588,7 +450,6 @@ var SchemaMessage = class {
|
|
|
588
450
|
bookmarkProperties,
|
|
589
451
|
displayLabel,
|
|
590
452
|
description,
|
|
591
|
-
relationships,
|
|
592
453
|
propertiesMetadata
|
|
593
454
|
} = opts;
|
|
594
455
|
this.stream = stream;
|
|
@@ -597,7 +458,6 @@ var SchemaMessage = class {
|
|
|
597
458
|
this.bookmarkProperties = bookmarkProperties;
|
|
598
459
|
this.displayLabel = displayLabel;
|
|
599
460
|
this.description = description;
|
|
600
|
-
this.relationships = relationships;
|
|
601
461
|
this.propertiesMetadata = propertiesMetadata;
|
|
602
462
|
}
|
|
603
463
|
toString() {
|
|
@@ -607,8 +467,7 @@ var SchemaMessage = class {
|
|
|
607
467
|
schema: this.schema,
|
|
608
468
|
key_properties: this.keyProperties,
|
|
609
469
|
label: this.displayLabel,
|
|
610
|
-
description: this.description
|
|
611
|
-
relationships: this.relationships
|
|
470
|
+
description: this.description
|
|
612
471
|
};
|
|
613
472
|
if (this.bookmarkProperties) {
|
|
614
473
|
result["bookmark_properties"] = this.bookmarkProperties;
|
|
@@ -617,148 +476,28 @@ var SchemaMessage = class {
|
|
|
617
476
|
}
|
|
618
477
|
};
|
|
619
478
|
|
|
620
|
-
// src/sdk/models/metadata.ts
|
|
621
|
-
var MetadataMap = class extends Map {
|
|
622
|
-
areEquals(array1, array2) {
|
|
623
|
-
return array1.length === array2.length && array1.every(function(value, index) {
|
|
624
|
-
return value === array2[index];
|
|
625
|
-
});
|
|
626
|
-
}
|
|
627
|
-
findExistingEntry(key) {
|
|
628
|
-
return Array.from(super.entries()).find((entry) => {
|
|
629
|
-
if (this.areEquals(entry[0], key)) {
|
|
630
|
-
return true;
|
|
631
|
-
}
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
get(key) {
|
|
635
|
-
const existingEntry = this.findExistingEntry(key);
|
|
636
|
-
return existingEntry?.[1];
|
|
637
|
-
}
|
|
638
|
-
set(key, value) {
|
|
639
|
-
const existingKey = this.findExistingEntry(key);
|
|
640
|
-
if (existingKey) {
|
|
641
|
-
return super.set(existingKey?.[0], value);
|
|
642
|
-
} else {
|
|
643
|
-
return super.set(key, value);
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
};
|
|
647
|
-
var StreamMetadata = class {
|
|
648
|
-
metadataByBreadcrumb;
|
|
649
|
-
constructor() {
|
|
650
|
-
this.metadataByBreadcrumb = new MetadataMap();
|
|
651
|
-
}
|
|
652
|
-
fromCatalogStreamMetadata(metadataArr) {
|
|
653
|
-
metadataArr.forEach((metadata) => {
|
|
654
|
-
this.metadataByBreadcrumb.set(metadata.breadcrumb, metadata.metadata);
|
|
655
|
-
});
|
|
656
|
-
return this;
|
|
657
|
-
}
|
|
658
|
-
toString() {
|
|
659
|
-
return this.metadataByBreadcrumb;
|
|
660
|
-
}
|
|
661
|
-
get(breadcrumb) {
|
|
662
|
-
return this.metadataByBreadcrumb.get(breadcrumb);
|
|
663
|
-
}
|
|
664
|
-
write(breadcrumb, key, value) {
|
|
665
|
-
if (value === void 0) {
|
|
666
|
-
throw new Error(`The value you're trying to write on the metadata at breadcrumb: \`${breadcrumb}\` and key: \`${key}\` is undefined.
|
|
667
|
-
|
|
668
|
-
Writing an undefined value is not a valid operation. Is there something wrong with your value?`);
|
|
669
|
-
}
|
|
670
|
-
if (!this.metadataByBreadcrumb.get(breadcrumb)) {
|
|
671
|
-
this.metadataByBreadcrumb.set(breadcrumb, {});
|
|
672
|
-
}
|
|
673
|
-
const previousValue = this.metadataByBreadcrumb.get(breadcrumb);
|
|
674
|
-
const newValue = {};
|
|
675
|
-
newValue[key] = value;
|
|
676
|
-
this.metadataByBreadcrumb.set(breadcrumb, { ...previousValue, ...newValue });
|
|
677
|
-
return this;
|
|
678
|
-
}
|
|
679
|
-
getRootBreadcrumbMetadata() {
|
|
680
|
-
const rootBreadcrumbMetadata = this.metadataByBreadcrumb.get([]);
|
|
681
|
-
if (!rootBreadcrumbMetadata) {
|
|
682
|
-
throw new Error(`No metadata was attached to the root breadcrumb \`[\`] while we need one to store metadata at the table level.
|
|
683
|
-
|
|
684
|
-
Is your catalog properly generated?`);
|
|
685
|
-
}
|
|
686
|
-
return rootBreadcrumbMetadata;
|
|
687
|
-
}
|
|
688
|
-
isStreamSelected() {
|
|
689
|
-
const rootBreadcrumbMetadata = this.getRootBreadcrumbMetadata();
|
|
690
|
-
if (rootBreadcrumbMetadata.selected === false) {
|
|
691
|
-
return false;
|
|
692
|
-
}
|
|
693
|
-
if (rootBreadcrumbMetadata.selected === true || rootBreadcrumbMetadata["selected-by-default"] === true) {
|
|
694
|
-
return true;
|
|
695
|
-
}
|
|
696
|
-
return false;
|
|
697
|
-
}
|
|
698
|
-
getKeyProperties() {
|
|
699
|
-
const rootBreadcrumbMetadata = this.getRootBreadcrumbMetadata();
|
|
700
|
-
if (!rootBreadcrumbMetadata["table-key-properties"]) {
|
|
701
|
-
return [];
|
|
702
|
-
}
|
|
703
|
-
return rootBreadcrumbMetadata["table-key-properties"];
|
|
704
|
-
}
|
|
705
|
-
getReplicationKey() {
|
|
706
|
-
const rootBreadcrumbMetadata = this.getRootBreadcrumbMetadata();
|
|
707
|
-
if (!rootBreadcrumbMetadata["replication-key"]) {
|
|
708
|
-
return void 0;
|
|
709
|
-
}
|
|
710
|
-
return rootBreadcrumbMetadata["replication-key"];
|
|
711
|
-
}
|
|
712
|
-
getReplicationMethod() {
|
|
713
|
-
const rootBreadcrumbMetadata = this.getRootBreadcrumbMetadata();
|
|
714
|
-
if (!rootBreadcrumbMetadata["replication-method"]) {
|
|
715
|
-
return void 0;
|
|
716
|
-
}
|
|
717
|
-
return rootBreadcrumbMetadata["replication-method"];
|
|
718
|
-
}
|
|
719
|
-
getForcedReplicationMethod() {
|
|
720
|
-
const rootBreadcrumbMetadata = this.getRootBreadcrumbMetadata();
|
|
721
|
-
if (!rootBreadcrumbMetadata["forced-replication-method"]) {
|
|
722
|
-
return void 0;
|
|
723
|
-
}
|
|
724
|
-
return rootBreadcrumbMetadata["forced-replication-method"];
|
|
725
|
-
}
|
|
726
|
-
toList() {
|
|
727
|
-
return Array.from(this.metadataByBreadcrumb.keys()).map((breadcrumb) => {
|
|
728
|
-
const metadata = this.metadataByBreadcrumb.get(breadcrumb);
|
|
729
|
-
if (!metadata) {
|
|
730
|
-
throw new Error(`There was no metadata entry for breadcrumb: \`${breadcrumb}\` in metadata map: ${this.metadataByBreadcrumb.toString()}`);
|
|
731
|
-
}
|
|
732
|
-
return {
|
|
733
|
-
breadcrumb,
|
|
734
|
-
metadata
|
|
735
|
-
};
|
|
736
|
-
});
|
|
737
|
-
}
|
|
738
|
-
};
|
|
739
|
-
|
|
740
479
|
// src/sdk/models/state.ts
|
|
741
|
-
import
|
|
480
|
+
import dayjs from "dayjs";
|
|
742
481
|
|
|
743
482
|
// src/sdk/constants/date.ts
|
|
744
483
|
var defaultDateTimeFormat = "YYYY-MM-DDTHH:mm:ss.SSSSSSZ";
|
|
745
484
|
|
|
746
485
|
// src/sdk/models/state.ts
|
|
747
|
-
import { format } from "util";
|
|
486
|
+
import { format as format2 } from "util";
|
|
748
487
|
var PROGRESS_MARKERS = "progressMarkers";
|
|
749
488
|
var PROGRESS_MARKER_NOTE = "Note";
|
|
750
489
|
var SIGNPOST_MARKER = "replicationKeySignpost";
|
|
751
490
|
var incrementStreamState = (state, replicationKey, latestRecord, isSorted) => {
|
|
752
491
|
let newRkValue = latestRecord[replicationKey];
|
|
753
492
|
if (!newRkValue) {
|
|
754
|
-
throw new Error(
|
|
493
|
+
throw new Error(format2(`No value for replicationKey: ${replicationKey} in record with keys: %j`, Object.keys(latestRecord)));
|
|
755
494
|
}
|
|
756
|
-
let newRkValueMoment =
|
|
495
|
+
let newRkValueMoment = dayjs(newRkValue);
|
|
757
496
|
let prevRkValue;
|
|
758
497
|
let prevRkValueMoment;
|
|
759
498
|
if (isSorted) {
|
|
760
499
|
prevRkValue = state["replicationKeyValue"];
|
|
761
|
-
prevRkValueMoment =
|
|
500
|
+
prevRkValueMoment = dayjs(prevRkValue);
|
|
762
501
|
if (prevRkValue && prevRkValueMoment.isAfter(newRkValueMoment)) {
|
|
763
502
|
throw new Error(
|
|
764
503
|
`Unsorted data detected in stream. Latest value '${newRkValue}' is
|
|
@@ -773,24 +512,24 @@ var incrementStreamState = (state, replicationKey, latestRecord, isSorted) => {
|
|
|
773
512
|
state[PROGRESS_MARKERS] = marker;
|
|
774
513
|
}
|
|
775
514
|
prevRkValue = state[PROGRESS_MARKERS]["replicationKeyValue"];
|
|
776
|
-
prevRkValueMoment =
|
|
515
|
+
prevRkValueMoment = dayjs(prevRkValue || state["replicationKeyValue"]);
|
|
777
516
|
if (prevRkValue && prevRkValueMoment.isAfter(newRkValueMoment)) {
|
|
778
517
|
newRkValueMoment = prevRkValueMoment;
|
|
779
518
|
newRkValue = prevRkValue;
|
|
780
519
|
}
|
|
781
520
|
}
|
|
782
521
|
const signpostMarker = state[SIGNPOST_MARKER];
|
|
783
|
-
const replicationKeySignpostMoment =
|
|
522
|
+
const replicationKeySignpostMoment = dayjs(signpostMarker);
|
|
784
523
|
if (replicationKeySignpostMoment && replicationKeySignpostMoment.isBefore(newRkValueMoment)) {
|
|
785
524
|
newRkValueMoment = replicationKeySignpostMoment;
|
|
786
525
|
newRkValue = replicationKeySignpostMoment.format(defaultDateTimeFormat);
|
|
787
526
|
}
|
|
788
527
|
if (isSorted === false) {
|
|
789
528
|
state[PROGRESS_MARKERS]["replicationKey"] = replicationKey;
|
|
790
|
-
state[PROGRESS_MARKERS]["replicationKeyValue"] =
|
|
529
|
+
state[PROGRESS_MARKERS]["replicationKeyValue"] = dayjs(newRkValueMoment).format(defaultDateTimeFormat);
|
|
791
530
|
} else {
|
|
792
531
|
state["replicationKey"] = replicationKey;
|
|
793
|
-
state["replicationKeyValue"] =
|
|
532
|
+
state["replicationKeyValue"] = dayjs(newRkValueMoment).format(defaultDateTimeFormat);
|
|
794
533
|
}
|
|
795
534
|
return state;
|
|
796
535
|
};
|
|
@@ -812,7 +551,7 @@ var finalizeStateProgressMarkers = (state) => {
|
|
|
812
551
|
return state;
|
|
813
552
|
};
|
|
814
553
|
var _greaterThan = (aValue, bValue) => {
|
|
815
|
-
return
|
|
554
|
+
return dayjs(aValue).isAfter(dayjs(bValue));
|
|
816
555
|
};
|
|
817
556
|
var extractStateForStream = (state, streamId) => {
|
|
818
557
|
if (!state.bookmarks) {
|
|
@@ -835,26 +574,16 @@ var StateService = class {
|
|
|
835
574
|
}
|
|
836
575
|
return this.instance;
|
|
837
576
|
}
|
|
838
|
-
|
|
839
|
-
setBookmark(streamId, ts) {
|
|
840
|
-
this.bookmarks[streamId] = ts.format(defaultDateTimeFormat);
|
|
841
|
-
}
|
|
842
|
-
setBookmarkV2(streamId, streamState) {
|
|
577
|
+
setBookmark(streamId, streamState) {
|
|
843
578
|
this.bookmarks[streamId] = streamState;
|
|
844
579
|
}
|
|
845
|
-
|
|
580
|
+
setBookmarkSignpost(streamId, signpostValue) {
|
|
846
581
|
if (!this.bookmarks[streamId]) {
|
|
847
582
|
this.bookmarks[streamId] = {};
|
|
848
583
|
}
|
|
849
584
|
this.bookmarks[streamId][SIGNPOST_MARKER] = signpostValue;
|
|
850
585
|
}
|
|
851
586
|
getBookmark(streamId) {
|
|
852
|
-
if (!this.bookmarks[streamId]) {
|
|
853
|
-
return moment(0);
|
|
854
|
-
}
|
|
855
|
-
return moment(this.bookmarks[streamId]);
|
|
856
|
-
}
|
|
857
|
-
getBookmarkV2(streamId) {
|
|
858
587
|
if (this.bookmarks[streamId]) {
|
|
859
588
|
return this.bookmarks[streamId];
|
|
860
589
|
} else {
|
|
@@ -894,7 +623,7 @@ import axios2 from "axios";
|
|
|
894
623
|
import * as qs2 from "qs";
|
|
895
624
|
|
|
896
625
|
// src/sdk/models/tap/stream.ts
|
|
897
|
-
import
|
|
626
|
+
import dayjs2 from "dayjs";
|
|
898
627
|
|
|
899
628
|
// src/sdk/helpers/typing.ts
|
|
900
629
|
var isDatetimeType = (typeDef) => {
|
|
@@ -917,10 +646,11 @@ var isDatetimeType = (typeDef) => {
|
|
|
917
646
|
};
|
|
918
647
|
|
|
919
648
|
// src/sdk/models/tap/stream.ts
|
|
920
|
-
import
|
|
649
|
+
import cloneDeep2 from "lodash/cloneDeep.js";
|
|
921
650
|
import Bluebird2 from "bluebird";
|
|
922
651
|
|
|
923
652
|
// src/sdk/models/tap/tap.ts
|
|
653
|
+
import cloneDeep from "lodash/cloneDeep.js";
|
|
924
654
|
import Bluebird from "bluebird";
|
|
925
655
|
var DEFAULT_MAX_CONCURRENT_STREAMS = 5;
|
|
926
656
|
var Tap = class {
|
|
@@ -928,12 +658,21 @@ var Tap = class {
|
|
|
928
658
|
config;
|
|
929
659
|
streams;
|
|
930
660
|
concurrency = DEFAULT_MAX_CONCURRENT_STREAMS;
|
|
931
|
-
|
|
661
|
+
stateProvider;
|
|
662
|
+
tapState;
|
|
663
|
+
constructor(target, config, stateProvider) {
|
|
932
664
|
this.streams = [];
|
|
933
665
|
this.target = target;
|
|
934
666
|
this.config = config;
|
|
667
|
+
this.stateProvider = stateProvider;
|
|
668
|
+
this.tapState = { bookmarks: {} };
|
|
935
669
|
}
|
|
936
670
|
sync = async (options) => {
|
|
671
|
+
const initialState = await this.stateProvider.getState();
|
|
672
|
+
if (initialState.state && initialState.state.bookmarks) {
|
|
673
|
+
this.tapState = cloneDeep(initialState.state);
|
|
674
|
+
}
|
|
675
|
+
await this.init();
|
|
937
676
|
logger.info(`\u{1F680} Start syncing`);
|
|
938
677
|
const state = StateService.getInstance().get();
|
|
939
678
|
logger.info(`\u{1F4CD} Received state: %j`, state);
|
|
@@ -964,15 +703,13 @@ var Tap = class {
|
|
|
964
703
|
};
|
|
965
704
|
|
|
966
705
|
// src/sdk/models/tap/stream.ts
|
|
967
|
-
import { format as
|
|
706
|
+
import { format as format3 } from "util";
|
|
968
707
|
var Stream = class {
|
|
969
708
|
// Runtime values, can't be overriden
|
|
970
709
|
config;
|
|
971
710
|
tapState;
|
|
972
711
|
target;
|
|
973
|
-
|
|
974
|
-
// Replication method that can be forced in the stream implemenation. Will prevail over any user/catalog choice
|
|
975
|
-
forcedReplicationMethod = void 0;
|
|
712
|
+
replicationMethod = void 0;
|
|
976
713
|
selectedByDefault = true;
|
|
977
714
|
STATE_MSG_FREQUENCY = 10;
|
|
978
715
|
streamId = "default";
|
|
@@ -980,10 +717,9 @@ var Stream = class {
|
|
|
980
717
|
displayLabel = void 0;
|
|
981
718
|
description = void 0;
|
|
982
719
|
primaryKey = [];
|
|
983
|
-
|
|
720
|
+
replicationKey = void 0;
|
|
984
721
|
// When set, use the state from another stream for this stream
|
|
985
722
|
useStateFromStreamId = void 0;
|
|
986
|
-
relationships = [];
|
|
987
723
|
children = [];
|
|
988
724
|
// If true, it means that the records in this stream are sorted by replicationKey.
|
|
989
725
|
// If false, then the records are coming in an unsorted manner.
|
|
@@ -995,9 +731,9 @@ var Stream = class {
|
|
|
995
731
|
// Metrics configuration
|
|
996
732
|
rowsSyncedMetricsConf;
|
|
997
733
|
executionTimeMetricsConf;
|
|
998
|
-
constructor(config,
|
|
734
|
+
constructor(config, tapState, target, childConcurrency = DEFAULT_MAX_CONCURRENT_STREAMS) {
|
|
999
735
|
this.config = config;
|
|
1000
|
-
this.tapState =
|
|
736
|
+
this.tapState = cloneDeep2(tapState);
|
|
1001
737
|
this.target = target;
|
|
1002
738
|
this.childConcurrency = childConcurrency;
|
|
1003
739
|
this.rowsSyncedMetricsConf = {
|
|
@@ -1029,7 +765,7 @@ var Stream = class {
|
|
|
1029
765
|
return this.flush();
|
|
1030
766
|
} catch (err) {
|
|
1031
767
|
if (err instanceof Error) {
|
|
1032
|
-
throw new Error(
|
|
768
|
+
throw new Error(format3(`got an error while syncing streamId=%s, err: %s`, this.streamId, err.message));
|
|
1033
769
|
}
|
|
1034
770
|
throw err;
|
|
1035
771
|
}
|
|
@@ -1070,13 +806,11 @@ var Stream = class {
|
|
|
1070
806
|
if (!this.isSilent) {
|
|
1071
807
|
const schema = await this.getSchema();
|
|
1072
808
|
if (schema !== void 0) {
|
|
1073
|
-
const relatedRels = findRelatedRelationships(this.streamId, this.relationships);
|
|
1074
809
|
const replicationConfig = this.configuredReplicationConfig();
|
|
1075
810
|
const message = new SchemaMessage({
|
|
1076
811
|
keyProperties: this.primaryKey,
|
|
1077
812
|
stream: this.streamId,
|
|
1078
813
|
schema: schema.jsonSchema,
|
|
1079
|
-
relationships: relatedRels,
|
|
1080
814
|
...replicationConfig.replicationKey ? { bookmarkProperties: [replicationConfig.replicationKey] } : {},
|
|
1081
815
|
...this.displayLabel ? { displayLabel: this.displayLabel } : {},
|
|
1082
816
|
...this.description ? { description: this.description } : {},
|
|
@@ -1096,7 +830,7 @@ var Stream = class {
|
|
|
1096
830
|
const signpostMoment = await this.getReplicationKeySignpost();
|
|
1097
831
|
const signpostValue = signpostMoment?.format(defaultDateTimeFormat);
|
|
1098
832
|
if (signpostValue) {
|
|
1099
|
-
StateService.getInstance().
|
|
833
|
+
StateService.getInstance().setBookmarkSignpost(this.streamId, signpostValue);
|
|
1100
834
|
}
|
|
1101
835
|
};
|
|
1102
836
|
/**
|
|
@@ -1112,7 +846,7 @@ var Stream = class {
|
|
|
1112
846
|
if (this.useStateFromStreamId) {
|
|
1113
847
|
return;
|
|
1114
848
|
}
|
|
1115
|
-
if (this.
|
|
849
|
+
if (this.replicationMethod && !this.replicationKey) {
|
|
1116
850
|
return;
|
|
1117
851
|
}
|
|
1118
852
|
if (!replicationConfig.replicationKey) {
|
|
@@ -1122,14 +856,14 @@ var Stream = class {
|
|
|
1122
856
|
);
|
|
1123
857
|
}
|
|
1124
858
|
const stateServiceInst = StateService.getInstance();
|
|
1125
|
-
const state = stateServiceInst.
|
|
859
|
+
const state = stateServiceInst.getBookmark(this.streamId);
|
|
1126
860
|
const newState = incrementStreamState(
|
|
1127
861
|
state,
|
|
1128
862
|
replicationConfig.replicationKey,
|
|
1129
863
|
latestRecord,
|
|
1130
864
|
this.isSorted
|
|
1131
865
|
);
|
|
1132
|
-
StateService.getInstance().
|
|
866
|
+
StateService.getInstance().setBookmark(this.streamId, newState);
|
|
1133
867
|
}
|
|
1134
868
|
}
|
|
1135
869
|
};
|
|
@@ -1169,19 +903,19 @@ var Stream = class {
|
|
|
1169
903
|
this.replicationKey: ${replicationConfig.replicationKey}`);
|
|
1170
904
|
if (state.replicationKeyValue) {
|
|
1171
905
|
logger.debug(`${this.streamId} - getStartingTimestamp -> Returning replication key value: ${state.replicationKeyValue}`);
|
|
1172
|
-
return
|
|
906
|
+
return dayjs2(state.replicationKeyValue);
|
|
1173
907
|
} else if (startDate) {
|
|
1174
908
|
logger.debug(`${this.streamId} - getStartingTimestamp -> Returning config start date: ${startDate}`);
|
|
1175
|
-
return
|
|
909
|
+
return dayjs2(startDate);
|
|
1176
910
|
} else {
|
|
1177
911
|
logger.debug(`${this.streamId} - getStartingTimestamp -> Returning EPOCH (1970)`);
|
|
1178
|
-
return
|
|
912
|
+
return dayjs2(0);
|
|
1179
913
|
}
|
|
1180
914
|
}
|
|
1181
915
|
/**
|
|
1182
916
|
* Return the max allowable bookmark value for this stream's replication key.
|
|
1183
917
|
*
|
|
1184
|
-
* For timestamp-based replication keys, this defaults to `
|
|
918
|
+
* For timestamp-based replication keys, this defaults to `dayjs()`. For
|
|
1185
919
|
* non-timestamp replication keys, default to `undefined`.
|
|
1186
920
|
*
|
|
1187
921
|
* Override this value to prevent bookmarks from being advanced in cases where we
|
|
@@ -1189,7 +923,7 @@ var Stream = class {
|
|
|
1189
923
|
*/
|
|
1190
924
|
getReplicationKeySignpost = async () => {
|
|
1191
925
|
if (await this.isTimestampReplicationKey()) {
|
|
1192
|
-
return
|
|
926
|
+
return dayjs2();
|
|
1193
927
|
}
|
|
1194
928
|
return void 0;
|
|
1195
929
|
};
|
|
@@ -1197,22 +931,22 @@ var Stream = class {
|
|
|
1197
931
|
* Return the default replication method for the stream that will be used to write the catalog.
|
|
1198
932
|
*/
|
|
1199
933
|
defaultReplicationConfig = () => {
|
|
1200
|
-
if (this.
|
|
934
|
+
if (this.replicationMethod) {
|
|
1201
935
|
return {
|
|
1202
|
-
replicationMethod: this.
|
|
1203
|
-
replicationKey: this.
|
|
936
|
+
replicationMethod: this.replicationMethod,
|
|
937
|
+
replicationKey: this.replicationKey,
|
|
1204
938
|
isForced: true
|
|
1205
939
|
};
|
|
1206
940
|
}
|
|
1207
|
-
if (this.
|
|
941
|
+
if (this.replicationKey || this.useStateFromStreamId) {
|
|
1208
942
|
return {
|
|
1209
|
-
replicationMethod: "INCREMENTAL"
|
|
1210
|
-
replicationKey: this.
|
|
943
|
+
replicationMethod: "INCREMENTAL" /* INCREMENTAL */,
|
|
944
|
+
replicationKey: this.replicationKey,
|
|
1211
945
|
isForced: true
|
|
1212
946
|
};
|
|
1213
947
|
}
|
|
1214
948
|
return {
|
|
1215
|
-
replicationMethod: "FULL_TABLE"
|
|
949
|
+
replicationMethod: "FULL_TABLE" /* FULL_TABLE */,
|
|
1216
950
|
replicationKey: void 0,
|
|
1217
951
|
isForced: false
|
|
1218
952
|
};
|
|
@@ -1273,9 +1007,9 @@ var Stream = class {
|
|
|
1273
1007
|
rowsSent += 1;
|
|
1274
1008
|
}
|
|
1275
1009
|
const stateServiceInst = StateService.getInstance();
|
|
1276
|
-
const state = stateServiceInst.
|
|
1010
|
+
const state = stateServiceInst.getBookmark(this.streamId);
|
|
1277
1011
|
const newState = finalizeStateProgressMarkers(state);
|
|
1278
|
-
stateServiceInst.
|
|
1012
|
+
stateServiceInst.setBookmark(this.streamId, newState);
|
|
1279
1013
|
if (!parent) {
|
|
1280
1014
|
logger.info(`\u2705 Completed sync for stream: ${this.streamId} (${rowsSent} records)`);
|
|
1281
1015
|
} else {
|
|
@@ -1381,7 +1115,7 @@ function getTruncatedParamsForLog(params) {
|
|
|
1381
1115
|
}
|
|
1382
1116
|
|
|
1383
1117
|
// src/sdk/models/tap/restStream.ts
|
|
1384
|
-
import
|
|
1118
|
+
import cloneDeep3 from "lodash/cloneDeep.js";
|
|
1385
1119
|
import isEqual from "lodash/isEqual.js";
|
|
1386
1120
|
var RESTStream = class extends Stream {
|
|
1387
1121
|
axiosInstance;
|
|
@@ -1402,13 +1136,24 @@ var RESTStream = class extends Stream {
|
|
|
1402
1136
|
*/
|
|
1403
1137
|
_prepareRequest = async (nextPageToken, parent) => {
|
|
1404
1138
|
const method = this.httpMethod;
|
|
1405
|
-
const url = this.
|
|
1139
|
+
const url = this.getNextUrl(nextPageToken, parent);
|
|
1406
1140
|
if (!url) {
|
|
1407
1141
|
throw new Error(`StreamId: ${this.streamId} - URL is undefined. Did your properly define the URL for this stream?`);
|
|
1408
1142
|
}
|
|
1409
1143
|
const authenticator = this.authenticator;
|
|
1144
|
+
let finalUrl = url;
|
|
1145
|
+
let urlQueryParams = {};
|
|
1146
|
+
const queryIndex = url.indexOf("?");
|
|
1147
|
+
if (queryIndex !== -1) {
|
|
1148
|
+
finalUrl = url.substring(0, queryIndex);
|
|
1149
|
+
const rawQueryString = url.substring(queryIndex + 1);
|
|
1150
|
+
urlQueryParams = qs2.parse(rawQueryString);
|
|
1151
|
+
}
|
|
1410
1152
|
const params = {
|
|
1411
1153
|
...this.getNextUrlParams(nextPageToken, parent),
|
|
1154
|
+
// Query params embedded in URL should override computed pagination params
|
|
1155
|
+
// but should not override auth params which are appended last
|
|
1156
|
+
...urlQueryParams,
|
|
1412
1157
|
...await authenticator?.getAuthQS(this.config)
|
|
1413
1158
|
};
|
|
1414
1159
|
const requestBody = this.getRequestBodyForNextCall(nextPageToken, parent);
|
|
@@ -1427,7 +1172,7 @@ var RESTStream = class extends Stream {
|
|
|
1427
1172
|
return qs2.stringify(params2, { arrayFormat: "repeat" });
|
|
1428
1173
|
};
|
|
1429
1174
|
const request = {
|
|
1430
|
-
url,
|
|
1175
|
+
url: finalUrl,
|
|
1431
1176
|
method,
|
|
1432
1177
|
data: requestBody,
|
|
1433
1178
|
params,
|
|
@@ -1500,7 +1245,7 @@ var RESTStream = class extends Stream {
|
|
|
1500
1245
|
for await (const row of rows) {
|
|
1501
1246
|
yield row;
|
|
1502
1247
|
}
|
|
1503
|
-
const previousToken =
|
|
1248
|
+
const previousToken = cloneDeep3(nextPageToken);
|
|
1504
1249
|
nextPageToken = this.getNextPageToken(resp, previousToken);
|
|
1505
1250
|
if (nextPageToken && isEqual(nextPageToken, previousToken)) {
|
|
1506
1251
|
throw new Error(
|
|
@@ -1579,7 +1324,7 @@ var RESTStream = class extends Stream {
|
|
|
1579
1324
|
* TODO: Make URL + Path templatisable and replace those with Extractor (or Tap) config
|
|
1580
1325
|
* @returns
|
|
1581
1326
|
*/
|
|
1582
|
-
|
|
1327
|
+
getNextUrl(previousToken, parent) {
|
|
1583
1328
|
const urlPattern = [this.baseUrl, this.path].join("");
|
|
1584
1329
|
return urlPattern;
|
|
1585
1330
|
}
|
|
@@ -1760,6 +1505,8 @@ var StreamWarehouseSyncService = class {
|
|
|
1760
1505
|
mergeQueries = this.getReplaceQueries();
|
|
1761
1506
|
} else if (replicationMethod === "INCREMENTAL") {
|
|
1762
1507
|
mergeQueries = this.getMergeQueries();
|
|
1508
|
+
} else if (replicationMethod === "APPEND") {
|
|
1509
|
+
mergeQueries = this.getAppendQueries();
|
|
1763
1510
|
} else if (replicationMethod === "LOG_BASED") {
|
|
1764
1511
|
mergeQueries = this.getMergeQueries();
|
|
1765
1512
|
}
|
|
@@ -1931,25 +1678,25 @@ var flattenSchema = (streamId, schema) => {
|
|
|
1931
1678
|
|
|
1932
1679
|
// src/sdk/models/target/target.ts
|
|
1933
1680
|
import { Validator } from "jsonschema";
|
|
1934
|
-
import
|
|
1681
|
+
import dayjs4 from "dayjs";
|
|
1935
1682
|
|
|
1936
1683
|
// src/sdk/models/target/streamDbState.ts
|
|
1937
|
-
import
|
|
1684
|
+
import dayjs3 from "dayjs";
|
|
1938
1685
|
|
|
1939
1686
|
// src/sdk/models/target/temporaryFile.ts
|
|
1940
|
-
import
|
|
1687
|
+
import fs2 from "fs-extra";
|
|
1941
1688
|
import uniqueId from "lodash/uniqueId.js";
|
|
1942
1689
|
var createTemporaryFileStream = (streamId) => {
|
|
1943
|
-
|
|
1944
|
-
const
|
|
1945
|
-
const writeStream =
|
|
1690
|
+
fs2.ensureDirSync("./tmp");
|
|
1691
|
+
const path2 = `./tmp/${safePath(streamId)}-${uniqueId()}.tmp`;
|
|
1692
|
+
const writeStream = fs2.createWriteStream(path2, { flags: "w" });
|
|
1946
1693
|
return {
|
|
1947
1694
|
stream: writeStream,
|
|
1948
|
-
path
|
|
1695
|
+
path: path2
|
|
1949
1696
|
};
|
|
1950
1697
|
};
|
|
1951
|
-
function safePath(
|
|
1952
|
-
let formattedPath =
|
|
1698
|
+
function safePath(path2) {
|
|
1699
|
+
let formattedPath = path2;
|
|
1953
1700
|
formattedPath = formattedPath.replace(/^The\s+/gi, "");
|
|
1954
1701
|
formattedPath = formattedPath.replace(/\s+(?:\(|\[)?Dis(?:c|k) (?:One|Two|Three|Four|Five|Six|Seven|Eight|Nine|Ten|\d)+(?:\)|\])?/gi, "");
|
|
1955
1702
|
formattedPath = formattedPath.replace(/\s+/gi, "_");
|
|
@@ -1990,7 +1737,7 @@ function replaceSymbols(str) {
|
|
|
1990
1737
|
}
|
|
1991
1738
|
|
|
1992
1739
|
// src/sdk/models/target/streamDbState.ts
|
|
1993
|
-
import * as
|
|
1740
|
+
import * as fs3 from "fs";
|
|
1994
1741
|
var StreamState = class {
|
|
1995
1742
|
streamId;
|
|
1996
1743
|
schema;
|
|
@@ -2015,7 +1762,7 @@ var StreamState = class {
|
|
|
2015
1762
|
this.fileToLoad = createTemporaryFileStream(streamId);
|
|
2016
1763
|
this.syncedRowCount = 0;
|
|
2017
1764
|
this.keyProperties = [];
|
|
2018
|
-
this.batchDate =
|
|
1765
|
+
this.batchDate = dayjs3();
|
|
2019
1766
|
this.replicationMethod = replicationMethod;
|
|
2020
1767
|
this._hasBeenLoadedYetDuringThisSync = false;
|
|
2021
1768
|
}
|
|
@@ -2040,7 +1787,7 @@ var StreamState = class {
|
|
|
2040
1787
|
resetFileToLoad() {
|
|
2041
1788
|
this.fileToLoad.stream.close();
|
|
2042
1789
|
const oldPath = this.fileToLoad.path;
|
|
2043
|
-
|
|
1790
|
+
fs3.unlinkSync(oldPath);
|
|
2044
1791
|
this.fileToLoad = createTemporaryFileStream(this.streamId);
|
|
2045
1792
|
this.batchedRowCount = 0;
|
|
2046
1793
|
}
|
|
@@ -2071,8 +1818,8 @@ var semaphore = new Semaphore(1);
|
|
|
2071
1818
|
var ITarget = class _ITarget {
|
|
2072
1819
|
config;
|
|
2073
1820
|
schemaHooks;
|
|
2074
|
-
resolver;
|
|
2075
1821
|
syncTime;
|
|
1822
|
+
stateProvider;
|
|
2076
1823
|
// Latest state message received from the Tap
|
|
2077
1824
|
// Not yet flushed as we didn't upload the stream data since receiving it
|
|
2078
1825
|
batchedState;
|
|
@@ -2083,15 +1830,15 @@ var ITarget = class _ITarget {
|
|
|
2083
1830
|
streams;
|
|
2084
1831
|
// JSON Schema validator
|
|
2085
1832
|
validator;
|
|
2086
|
-
constructor(config,
|
|
1833
|
+
constructor(config, stateProvider) {
|
|
2087
1834
|
this.config = config;
|
|
2088
|
-
this.
|
|
2089
|
-
this.schemaHooks =
|
|
1835
|
+
this.stateProvider = stateProvider;
|
|
1836
|
+
this.schemaHooks = [];
|
|
2090
1837
|
this.streams = {};
|
|
2091
1838
|
this.batchedState = {};
|
|
2092
1839
|
this.flushedState = {};
|
|
2093
1840
|
this.validator = new Validator();
|
|
2094
|
-
this.syncTime =
|
|
1841
|
+
this.syncTime = dayjs4();
|
|
2095
1842
|
this.renameColumnStore = new RenameColumnStore();
|
|
2096
1843
|
}
|
|
2097
1844
|
///////////////////////////////////////////
|
|
@@ -2200,7 +1947,7 @@ var ITarget = class _ITarget {
|
|
|
2200
1947
|
this.streams[streamId] = new StreamState(
|
|
2201
1948
|
streamId,
|
|
2202
1949
|
dbSyncInstance,
|
|
2203
|
-
replicationMethod || "FULL_TABLE"
|
|
1950
|
+
replicationMethod || "FULL_TABLE" /* FULL_TABLE */
|
|
2204
1951
|
);
|
|
2205
1952
|
}
|
|
2206
1953
|
replicationMethod = (message) => {
|
|
@@ -2258,7 +2005,8 @@ var ITarget = class _ITarget {
|
|
|
2258
2005
|
}
|
|
2259
2006
|
});
|
|
2260
2007
|
streamState.setSchema(schema);
|
|
2261
|
-
|
|
2008
|
+
const streamReplicationMethod = this.streams[streamId]?.getReplicationMethod();
|
|
2009
|
+
if (message.keyProperties.length === 0 && streamReplicationMethod !== "APPEND" /* APPEND */) {
|
|
2262
2010
|
throw new Error(`StreamId: ${message.stream} - \`key_properties\` field is required and can't be empty in SCHEMA message.
|
|
2263
2011
|
Received SCHEMA message: ${JSON.stringify(message)}`);
|
|
2264
2012
|
}
|
|
@@ -2277,39 +2025,7 @@ var ITarget = class _ITarget {
|
|
|
2277
2025
|
return translation;
|
|
2278
2026
|
}
|
|
2279
2027
|
return k;
|
|
2280
|
-
})
|
|
2281
|
-
relationships: {
|
|
2282
|
-
left: message.relationships.left.flatMap((l) => {
|
|
2283
|
-
if (this.renameColumnStore.isReady(l.streamId) && this.renameColumnStore.isReady(message.stream)) {
|
|
2284
|
-
const from = this.renameColumnStore.getColumnTranslation(l.streamId, l.from);
|
|
2285
|
-
const to = this.renameColumnStore.getColumnTranslation(message.stream, l.to);
|
|
2286
|
-
if (from && to) {
|
|
2287
|
-
return [{
|
|
2288
|
-
type: l.type,
|
|
2289
|
-
streamId: l.streamId,
|
|
2290
|
-
from,
|
|
2291
|
-
to
|
|
2292
|
-
}];
|
|
2293
|
-
}
|
|
2294
|
-
}
|
|
2295
|
-
return [];
|
|
2296
|
-
}),
|
|
2297
|
-
right: message.relationships.right.flatMap((r) => {
|
|
2298
|
-
if (this.renameColumnStore.isReady(message.stream) && this.renameColumnStore.isReady(r.streamId)) {
|
|
2299
|
-
const from = this.renameColumnStore.getColumnTranslation(message.stream, r.from);
|
|
2300
|
-
const to = this.renameColumnStore.getColumnTranslation(r.streamId, r.to);
|
|
2301
|
-
if (from && to) {
|
|
2302
|
-
return [{
|
|
2303
|
-
type: r.type,
|
|
2304
|
-
streamId: r.streamId,
|
|
2305
|
-
from,
|
|
2306
|
-
to
|
|
2307
|
-
}];
|
|
2308
|
-
}
|
|
2309
|
-
}
|
|
2310
|
-
return [];
|
|
2311
|
-
})
|
|
2312
|
-
}
|
|
2028
|
+
})
|
|
2313
2029
|
};
|
|
2314
2030
|
await Bluebird3.map(this.schemaHooks, async (hook) => {
|
|
2315
2031
|
const input = {
|
|
@@ -2342,9 +2058,6 @@ var ITarget = class _ITarget {
|
|
|
2342
2058
|
try {
|
|
2343
2059
|
logger.info(`\u{1F44D} Data Extraction is complete. Will load remaining batched stream records.`);
|
|
2344
2060
|
await this.loadAllStreamsInWarehouse({ isFinalLoad: true });
|
|
2345
|
-
const configResolver = loadResolver();
|
|
2346
|
-
await configResolver.markSyncComplete();
|
|
2347
|
-
await configResolver.flushMetrics();
|
|
2348
2061
|
return Promise.resolve();
|
|
2349
2062
|
} catch (err) {
|
|
2350
2063
|
logger.error(`Error while handling complete event.`);
|
|
@@ -2357,7 +2070,8 @@ var ITarget = class _ITarget {
|
|
|
2357
2070
|
return false;
|
|
2358
2071
|
}
|
|
2359
2072
|
const batchedRecords = stream.getBatchedRowCount();
|
|
2360
|
-
|
|
2073
|
+
const method = stream.getReplicationMethod();
|
|
2074
|
+
if ((method === "INCREMENTAL" || method === "APPEND") && batchedRecords > 1e6) {
|
|
2361
2075
|
logger.info(`\u{1F9CA} Stream=${streamId} has reached the maximum amount of batched records. We'll trigger a flush of ALL streams in the Warehouse.`);
|
|
2362
2076
|
return true;
|
|
2363
2077
|
} else {
|
|
@@ -2387,8 +2101,7 @@ var ITarget = class _ITarget {
|
|
|
2387
2101
|
}
|
|
2388
2102
|
};
|
|
2389
2103
|
emitState = (state) => {
|
|
2390
|
-
|
|
2391
|
-
return writeStateFile(configResolver, JSON.stringify(state));
|
|
2104
|
+
return this.stateProvider.writeState(JSON.stringify(state));
|
|
2392
2105
|
};
|
|
2393
2106
|
uploadStreamsToWarehouse = async (streamsToUpload) => {
|
|
2394
2107
|
try {
|
|
@@ -2427,24 +2140,1089 @@ var ITarget = class _ITarget {
|
|
|
2427
2140
|
};
|
|
2428
2141
|
};
|
|
2429
2142
|
|
|
2430
|
-
// src/
|
|
2431
|
-
var
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2143
|
+
// src/sdk/file-processing/types.ts
|
|
2144
|
+
var FileFormat = /* @__PURE__ */ ((FileFormat2) => {
|
|
2145
|
+
FileFormat2["CSV"] = "CSV";
|
|
2146
|
+
FileFormat2["EXCEL"] = "EXCEL";
|
|
2147
|
+
return FileFormat2;
|
|
2148
|
+
})(FileFormat || {});
|
|
2149
|
+
|
|
2150
|
+
// src/sdk/file-processing/schema-utils.ts
|
|
2151
|
+
function fieldTypeToJsonSchema(fieldType) {
|
|
2152
|
+
switch (fieldType) {
|
|
2153
|
+
case "STRING":
|
|
2154
|
+
return { type: ["null", "string"] };
|
|
2155
|
+
case "FLOAT":
|
|
2156
|
+
return { type: ["null", "number"] };
|
|
2157
|
+
case "TIMESTAMP":
|
|
2158
|
+
return { type: ["null", "string"], format: "date-time" };
|
|
2159
|
+
default:
|
|
2160
|
+
return { type: ["null", "string"] };
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
function excelFieldsToJsonSchema(fields) {
|
|
2164
|
+
const properties = {};
|
|
2165
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
2166
|
+
properties[key] = fieldTypeToJsonSchema(field.type);
|
|
2167
|
+
}
|
|
2168
|
+
return { type: "object", properties };
|
|
2169
|
+
}
|
|
2170
|
+
function csvFieldsToJsonSchema(config) {
|
|
2171
|
+
const properties = {};
|
|
2172
|
+
if (Array.isArray(config)) {
|
|
2173
|
+
for (const field of config) {
|
|
2174
|
+
properties[field.key] = fieldTypeToJsonSchema(field.type);
|
|
2175
|
+
}
|
|
2176
|
+
} else {
|
|
2177
|
+
for (const [, field] of Object.entries(config)) {
|
|
2178
|
+
properties[field.key] = fieldTypeToJsonSchema(field.type);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
return { type: "object", properties };
|
|
2182
|
+
}
|
|
2183
|
+
function extractPrimaryKeysFromCsvConfig(config) {
|
|
2184
|
+
if (Array.isArray(config)) {
|
|
2185
|
+
return config.map((f) => f.key);
|
|
2186
|
+
}
|
|
2187
|
+
return Object.values(config).map((f) => f.key);
|
|
2188
|
+
}
|
|
2189
|
+
function extractPrimaryKeysFromExcelFields(fields) {
|
|
2190
|
+
return Object.entries(fields).filter(([, field]) => {
|
|
2191
|
+
return field.primaryKey === true || field.primaryKey === true;
|
|
2192
|
+
}).map(([key]) => key);
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
// src/sdk/file-processing/excel/reader.ts
|
|
2196
|
+
import XLSX from "xlsx";
|
|
2197
|
+
import path from "path";
|
|
2198
|
+
var excelColumnToIndex = (columnLetter) => {
|
|
2199
|
+
const letters = columnLetter.toUpperCase();
|
|
2200
|
+
let index = 0;
|
|
2201
|
+
for (let i = 0; i < letters.length; i++) {
|
|
2202
|
+
const charCode = letters.charCodeAt(i);
|
|
2203
|
+
if (charCode < 65 || charCode > 90) {
|
|
2204
|
+
throw new Error(`Invalid column letter: ${columnLetter}`);
|
|
2205
|
+
}
|
|
2206
|
+
index = index * 26 + (charCode - 64);
|
|
2207
|
+
}
|
|
2208
|
+
return index - 1;
|
|
2209
|
+
};
|
|
2210
|
+
var indexToExcelColumn = (columnIndex) => {
|
|
2211
|
+
if (columnIndex < 0) {
|
|
2212
|
+
throw new Error(`Invalid column index: ${columnIndex}. Must be >= 0`);
|
|
2213
|
+
}
|
|
2214
|
+
let result = "";
|
|
2215
|
+
let index = columnIndex;
|
|
2216
|
+
while (true) {
|
|
2217
|
+
result = String.fromCharCode(65 + index % 26) + result;
|
|
2218
|
+
index = Math.floor(index / 26);
|
|
2219
|
+
if (index === 0) break;
|
|
2220
|
+
index--;
|
|
2221
|
+
}
|
|
2222
|
+
return result;
|
|
2223
|
+
};
|
|
2224
|
+
var excelRowToIndex = (rowNumber) => {
|
|
2225
|
+
const rowNum = typeof rowNumber === "string" ? parseInt(rowNumber, 10) : rowNumber;
|
|
2226
|
+
if (isNaN(rowNum) || rowNum < 1) {
|
|
2227
|
+
throw new Error(`Invalid row number: ${rowNumber}. Must be >= 1`);
|
|
2228
|
+
}
|
|
2229
|
+
return rowNum - 1;
|
|
2230
|
+
};
|
|
2231
|
+
var indexToExcelRow = (index) => {
|
|
2232
|
+
if (index < 0) {
|
|
2233
|
+
throw new Error(`Invalid index: ${index}. Must be >= 0`);
|
|
2234
|
+
}
|
|
2235
|
+
return index + 1;
|
|
2236
|
+
};
|
|
2237
|
+
var parseCellReference = (cellRef) => {
|
|
2238
|
+
const match = cellRef.match(/^([A-Z]+)(\d+)$/);
|
|
2239
|
+
if (!match) {
|
|
2240
|
+
throw new Error(`Invalid cell reference: ${cellRef}. Expected format like "A1", "Z24", "AA100"`);
|
|
2241
|
+
}
|
|
2242
|
+
const [, columnLetter, rowNumber] = match;
|
|
2243
|
+
return {
|
|
2244
|
+
columnIndex: excelColumnToIndex(columnLetter),
|
|
2245
|
+
rowIndex: excelRowToIndex(rowNumber)
|
|
2246
|
+
};
|
|
2247
|
+
};
|
|
2248
|
+
var createCellReference = (columnIndex, rowIndex) => {
|
|
2249
|
+
return indexToExcelColumn(columnIndex) + indexToExcelRow(rowIndex);
|
|
2250
|
+
};
|
|
2251
|
+
var extractSingleSheetRows = async (fileName, conf) => {
|
|
2252
|
+
console.log("Extracting single sheet rows from file: %s", fileName);
|
|
2253
|
+
const workbook = XLSX.readFile(fileName, { dense: true });
|
|
2254
|
+
const sheetKeys = Object.keys(workbook.Sheets);
|
|
2255
|
+
const sheet = conf.sheetName ? workbook.Sheets[conf.sheetName] : workbook.Sheets[sheetKeys[0]];
|
|
2256
|
+
if (!sheet) {
|
|
2257
|
+
throw new Error(`Sheet ${conf.sheetName} not found in workbook`);
|
|
2258
|
+
}
|
|
2259
|
+
const sheetData = sheet["!data"];
|
|
2260
|
+
if (!sheetData) {
|
|
2261
|
+
throw new Error(`No data in ${conf.sheetName || "first"} sheet`);
|
|
2262
|
+
}
|
|
2263
|
+
if (sheetData.length === 0) {
|
|
2264
|
+
throw new Error(`Sheet ${conf.sheetName || "first"} is empty`);
|
|
2265
|
+
}
|
|
2266
|
+
const variables = conf.fileNameVariablesExtractor(fileName, workbook);
|
|
2267
|
+
const data = [];
|
|
2268
|
+
let rowIdx = 0;
|
|
2269
|
+
const minColumnIndex = Object.values(conf.columns).reduce((min, column) => {
|
|
2270
|
+
return column.column ? Math.min(min, excelColumnToIndex(column.column)) : min;
|
|
2271
|
+
}, Number.MAX_SAFE_INTEGER);
|
|
2272
|
+
for (const row of sheetData) {
|
|
2273
|
+
if (rowIdx < conf.numberOfRowsToSkip) {
|
|
2274
|
+
rowIdx++;
|
|
2275
|
+
continue;
|
|
2276
|
+
}
|
|
2277
|
+
if (!row) {
|
|
2278
|
+
console.log("Empty row at index: %s, skipped processing.", rowIdx);
|
|
2279
|
+
continue;
|
|
2280
|
+
}
|
|
2281
|
+
if (minColumnIndex !== Number.MAX_SAFE_INTEGER && !row[minColumnIndex]?.v) {
|
|
2282
|
+
console.log("stopping processing because of empty value in the first data column (idx: %s)", minColumnIndex);
|
|
2283
|
+
break;
|
|
2284
|
+
}
|
|
2285
|
+
const rowData = Object.entries(conf.columns).reduce((acc, [key, column]) => {
|
|
2286
|
+
if (column.variableName) {
|
|
2287
|
+
if (!variables) {
|
|
2288
|
+
throw new Error(`No variables extracted from filename, cannot extract derived field ${key}`);
|
|
2289
|
+
}
|
|
2290
|
+
acc[key] = variables[column.variableName] || "";
|
|
2291
|
+
} else {
|
|
2292
|
+
const colIndex = excelColumnToIndex(column.column);
|
|
2293
|
+
acc[key] = row[colIndex]?.v || "";
|
|
2294
|
+
}
|
|
2295
|
+
return acc;
|
|
2296
|
+
}, {});
|
|
2297
|
+
data.push(rowData);
|
|
2298
|
+
rowIdx++;
|
|
2299
|
+
}
|
|
2300
|
+
console.log("Extracted %s rows from sheet %s", data.length, conf.sheetName || "first");
|
|
2301
|
+
return data;
|
|
2302
|
+
};
|
|
2303
|
+
var extractExcelRows = async (localFilePath, conf) => {
|
|
2304
|
+
let data;
|
|
2305
|
+
if (conf.type === "single-sheet-extraction") {
|
|
2306
|
+
data = await extractSingleSheetRows(localFilePath, conf);
|
|
2307
|
+
} else if (conf.type === "processor") {
|
|
2308
|
+
const workbook = XLSX.readFile(localFilePath, { dense: true });
|
|
2309
|
+
data = await conf.processor(workbook);
|
|
2310
|
+
} else {
|
|
2311
|
+
throw new Error(`Unsupported configuration type: ${conf.type}`);
|
|
2312
|
+
}
|
|
2313
|
+
return data;
|
|
2314
|
+
};
|
|
2315
|
+
var validateExcelConfig = (config) => {
|
|
2316
|
+
if (!config.extension) {
|
|
2317
|
+
throw new Error("Extension is required");
|
|
2318
|
+
}
|
|
2319
|
+
if (!config.tableName) {
|
|
2320
|
+
throw new Error("Table name is required");
|
|
2321
|
+
}
|
|
2322
|
+
if (!config.fileNameValidator || typeof config.fileNameValidator !== "function") {
|
|
2323
|
+
throw new Error("File name validator function is required");
|
|
2324
|
+
}
|
|
2325
|
+
if (!config.fileNameVariablesExtractor || typeof config.fileNameVariablesExtractor !== "function") {
|
|
2326
|
+
throw new Error("File name variables extractor function is required");
|
|
2327
|
+
}
|
|
2328
|
+
if (config.type === "single-sheet-extraction" && (!config.columns || Object.keys(config.columns).length === 0)) {
|
|
2329
|
+
throw new Error("At least one column configuration is required for single-sheet-extraction");
|
|
2330
|
+
}
|
|
2331
|
+
if (config.type === "single-sheet-extraction") {
|
|
2332
|
+
Object.entries(config.columns).forEach(([key, column]) => {
|
|
2333
|
+
if (!column.type) {
|
|
2334
|
+
throw new Error(`Column ${key} must have a type`);
|
|
2335
|
+
}
|
|
2336
|
+
if (column.column) {
|
|
2337
|
+
const colLetter = column.column;
|
|
2338
|
+
if (!/^[A-Z]+$/i.test(colLetter)) {
|
|
2339
|
+
throw new Error(`Invalid column letter format: ${colLetter}`);
|
|
2340
|
+
}
|
|
2341
|
+
} else if (column.variableName) {
|
|
2342
|
+
} else {
|
|
2343
|
+
throw new Error(`Column ${key} must specify either 'column' or 'variableName'`);
|
|
2344
|
+
}
|
|
2345
|
+
});
|
|
2346
|
+
if (config.numberOfRowsToSkip === void 0 || config.numberOfRowsToSkip < 0) {
|
|
2347
|
+
throw new Error("Number of rows to skip must be specified and non-negative");
|
|
2348
|
+
}
|
|
2349
|
+
} else if (config.type === "processor") {
|
|
2350
|
+
if (!config.processor || typeof config.processor !== "function") {
|
|
2351
|
+
throw new Error("Processor function is required for processor type");
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
};
|
|
2355
|
+
var findAllMatchingConfigs = (filename, configs) => {
|
|
2356
|
+
const fileInfo = path.parse(filename);
|
|
2357
|
+
return configs.filter(
|
|
2358
|
+
(config) => config.fileNameValidator(fileInfo.base) && config.extension === fileInfo.ext.replace(".", "")
|
|
2359
|
+
);
|
|
2360
|
+
};
|
|
2361
|
+
async function* createExcelGenerator(data) {
|
|
2362
|
+
for (const row of data) {
|
|
2363
|
+
yield row;
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
// src/sdk/file-processing/excel/config-builder.ts
|
|
2368
|
+
var ExcelExtractionConfigBuilder = class _ExcelExtractionConfigBuilder {
|
|
2369
|
+
config = {
|
|
2370
|
+
fileNameValidator: () => true,
|
|
2371
|
+
fileNameVariablesExtractor: () => ({}),
|
|
2372
|
+
replicationMethod: "FULL_TABLE" /* FULL_TABLE */
|
|
2373
|
+
};
|
|
2374
|
+
static create() {
|
|
2375
|
+
return new _ExcelExtractionConfigBuilder();
|
|
2376
|
+
}
|
|
2377
|
+
extension(ext) {
|
|
2378
|
+
this.config.extension = ext;
|
|
2379
|
+
return this;
|
|
2380
|
+
}
|
|
2381
|
+
tableName(name) {
|
|
2382
|
+
this.config.tableName = name;
|
|
2383
|
+
return this;
|
|
2384
|
+
}
|
|
2385
|
+
fileValidator(validator) {
|
|
2386
|
+
this.config.fileNameValidator = validator;
|
|
2387
|
+
return this;
|
|
2388
|
+
}
|
|
2389
|
+
variablesExtractor(extractor) {
|
|
2390
|
+
this.config.fileNameVariablesExtractor = extractor;
|
|
2391
|
+
return this;
|
|
2392
|
+
}
|
|
2393
|
+
replicationMethod(method) {
|
|
2394
|
+
this.config.replicationMethod = method;
|
|
2395
|
+
return this;
|
|
2396
|
+
}
|
|
2397
|
+
columns(cols) {
|
|
2398
|
+
if (this.config.type === "single-sheet-extraction") {
|
|
2399
|
+
this.config.columns = cols;
|
|
2400
|
+
} else {
|
|
2401
|
+
throw new Error("Columns can only be set for single-sheet-extraction");
|
|
2402
|
+
}
|
|
2403
|
+
return this;
|
|
2404
|
+
}
|
|
2405
|
+
singleSheet(sheetName, skipRows = 0) {
|
|
2406
|
+
this.config.type = "single-sheet-extraction";
|
|
2407
|
+
if (sheetName !== void 0) {
|
|
2408
|
+
this.config.sheetName = sheetName;
|
|
2409
|
+
}
|
|
2410
|
+
this.config.numberOfRowsToSkip = skipRows;
|
|
2411
|
+
return this;
|
|
2412
|
+
}
|
|
2413
|
+
processor(processorFn) {
|
|
2414
|
+
this.config.type = "processor";
|
|
2415
|
+
this.config.processor = processorFn;
|
|
2416
|
+
return this;
|
|
2417
|
+
}
|
|
2418
|
+
build() {
|
|
2419
|
+
if (!this.config.type) {
|
|
2420
|
+
throw new Error("Configuration type must be specified (singleSheet or processor)");
|
|
2421
|
+
}
|
|
2422
|
+
if (!this.config.extension) {
|
|
2423
|
+
throw new Error("Extension must be set");
|
|
2424
|
+
}
|
|
2425
|
+
if (!this.config.tableName) {
|
|
2426
|
+
throw new Error("Table name must be set");
|
|
2427
|
+
}
|
|
2428
|
+
if (!this.config.replicationMethod) {
|
|
2429
|
+
throw new Error("Replication method must be set");
|
|
2430
|
+
}
|
|
2431
|
+
if (typeof this.config.fileNameValidator !== "function") {
|
|
2432
|
+
throw new Error("fileNameValidator must be a function");
|
|
2433
|
+
}
|
|
2434
|
+
if (typeof this.config.fileNameVariablesExtractor !== "function") {
|
|
2435
|
+
throw new Error("fileNameVariablesExtractor must be a function");
|
|
2436
|
+
}
|
|
2437
|
+
if (this.config.type === "single-sheet-extraction") {
|
|
2438
|
+
const singleSheetConfig = this.config;
|
|
2439
|
+
if (!singleSheetConfig.columns || Object.keys(singleSheetConfig.columns).length === 0) {
|
|
2440
|
+
throw new Error("Columns must be set with at least one column for single-sheet-extraction");
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
if (this.config.type === "processor") {
|
|
2444
|
+
const processorConfig = this.config;
|
|
2445
|
+
if (typeof processorConfig.processor !== "function") {
|
|
2446
|
+
throw new Error("Processor function must be set for processor configs");
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
return this.config;
|
|
2450
|
+
}
|
|
2451
|
+
};
|
|
2452
|
+
|
|
2453
|
+
// src/sdk/file-processing/csv/reader.ts
|
|
2454
|
+
import csv from "csv-parser";
|
|
2455
|
+
import { createReadStream } from "fs";
|
|
2456
|
+
import { format as format4 } from "util";
|
|
2457
|
+
import { Readable, Transform } from "stream";
|
|
2458
|
+
import * as iconv from "iconv-lite";
|
|
2459
|
+
var logPrefix = "[csvReader]";
|
|
2460
|
+
var createStripBomTransform = () => {
|
|
2461
|
+
let isFirstChunk = true;
|
|
2462
|
+
return new Transform({
|
|
2463
|
+
transform(chunk2, encoding, callback) {
|
|
2464
|
+
if (isFirstChunk) {
|
|
2465
|
+
isFirstChunk = false;
|
|
2466
|
+
if (chunk2.length >= 3 && chunk2[0] === 239 && chunk2[1] === 187 && chunk2[2] === 191) {
|
|
2467
|
+
chunk2 = chunk2.slice(3);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
callback(null, chunk2);
|
|
2471
|
+
}
|
|
2472
|
+
});
|
|
2473
|
+
};
|
|
2474
|
+
var getStringHexInfo = (str) => {
|
|
2475
|
+
const bytes = Buffer.from(str, "utf8");
|
|
2476
|
+
const hex = bytes.toString("hex").match(/.{2}/g)?.join(" ") || "";
|
|
2477
|
+
return {
|
|
2478
|
+
original: str,
|
|
2479
|
+
length: str.length,
|
|
2480
|
+
hex,
|
|
2481
|
+
trimmed: str.trim()
|
|
2482
|
+
};
|
|
2483
|
+
};
|
|
2484
|
+
var formatHexComparison = (expected, actual) => {
|
|
2485
|
+
const expectedInfo = getStringHexInfo(expected);
|
|
2486
|
+
const actualInfo = getStringHexInfo(actual);
|
|
2487
|
+
return `
|
|
2488
|
+
Expected: "${expected}" (${expectedInfo.length} chars) [${expectedInfo.hex}]
|
|
2489
|
+
Actual: "${actual}" (${actualInfo.length} chars) [${actualInfo.hex}]`;
|
|
2490
|
+
};
|
|
2491
|
+
async function* rowGeneratorFromCsv(path2, fileConfig) {
|
|
2492
|
+
const isGeneratorConfigArray = Array.isArray(fileConfig.fields);
|
|
2493
|
+
const csvOptions = { separator: fileConfig.separator };
|
|
2494
|
+
if (isGeneratorConfigArray) {
|
|
2495
|
+
csvOptions.headers = false;
|
|
2496
|
+
}
|
|
2497
|
+
const csvStream = csv(csvOptions);
|
|
2498
|
+
const syncedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2499
|
+
let initialReadStream;
|
|
2500
|
+
let finalInputStream;
|
|
2501
|
+
const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
|
|
2502
|
+
if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
|
|
2503
|
+
initialReadStream = createReadStream(path2);
|
|
2504
|
+
finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
|
|
2505
|
+
} else {
|
|
2506
|
+
initialReadStream = createReadStream(path2);
|
|
2507
|
+
finalInputStream = initialReadStream.pipe(createStripBomTransform());
|
|
2508
|
+
}
|
|
2509
|
+
finalInputStream.pipe(csvStream);
|
|
2510
|
+
try {
|
|
2511
|
+
for await (const row of Readable.from(csvStream)) {
|
|
2512
|
+
if (!isGeneratorConfigArray) {
|
|
2513
|
+
const rowData = Object.keys(fileConfig.fields).reduce((acc, key) => {
|
|
2514
|
+
const keyGenerator = fileConfig.fields[key.trim()];
|
|
2515
|
+
if (!keyGenerator) {
|
|
2516
|
+
return acc;
|
|
2517
|
+
}
|
|
2518
|
+
return {
|
|
2519
|
+
...acc,
|
|
2520
|
+
[keyGenerator.key]: keyGenerator.valueTransformer ? keyGenerator.valueTransformer(row[key]) : row[key]
|
|
2521
|
+
};
|
|
2522
|
+
}, fileConfig.addSyncedAtColumn ? { _wly_synced_at: syncedAt } : {});
|
|
2523
|
+
yield rowData;
|
|
2524
|
+
} else {
|
|
2525
|
+
const rowData = Object.keys(row).reduce((acc, key, i) => {
|
|
2526
|
+
const keyGenerator = fileConfig.fields[i];
|
|
2527
|
+
if (!keyGenerator) {
|
|
2528
|
+
return acc;
|
|
2529
|
+
}
|
|
2530
|
+
return {
|
|
2531
|
+
...acc,
|
|
2532
|
+
[keyGenerator.key]: keyGenerator.valueTransformer ? keyGenerator.valueTransformer(row[i]) : row[i]
|
|
2533
|
+
};
|
|
2534
|
+
}, fileConfig.addSyncedAtColumn ? { _wly_synced_at: syncedAt } : {});
|
|
2535
|
+
yield rowData;
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
} finally {
|
|
2539
|
+
initialReadStream.destroy();
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
var checkCsvHeaderRow = async (path2, fileConfig) => {
|
|
2543
|
+
return new Promise((resolve, reject) => {
|
|
2544
|
+
const colsWithParsingConfig = Object.keys(fileConfig.fields).map((key) => key.trim());
|
|
2545
|
+
console.debug(`${logPrefix} CSV Expected columns:`, colsWithParsingConfig.map((col) => {
|
|
2546
|
+
const info = getStringHexInfo(col);
|
|
2547
|
+
return `"${info.original}" [${info.hex}]`;
|
|
2548
|
+
}));
|
|
2549
|
+
const isGeneratorConfigArray = Array.isArray(fileConfig.fields);
|
|
2550
|
+
const csvOptions2 = { separator: fileConfig.separator };
|
|
2551
|
+
if (isGeneratorConfigArray) {
|
|
2552
|
+
csvOptions2.headers = false;
|
|
2553
|
+
}
|
|
2554
|
+
const csvStream = csv(csvOptions2);
|
|
2555
|
+
let initialReadStream;
|
|
2556
|
+
let finalInputStream;
|
|
2557
|
+
const encodingToUse = fileConfig.encoding ? fileConfig.encoding.toLowerCase() : null;
|
|
2558
|
+
if (encodingToUse && encodingToUse !== "utf-8" && encodingToUse !== "utf8" && iconv.encodingExists(encodingToUse)) {
|
|
2559
|
+
initialReadStream = createReadStream(path2);
|
|
2560
|
+
finalInputStream = initialReadStream.pipe(iconv.decodeStream(encodingToUse));
|
|
2561
|
+
} else {
|
|
2562
|
+
initialReadStream = createReadStream(path2);
|
|
2563
|
+
finalInputStream = initialReadStream.pipe(createStripBomTransform());
|
|
2564
|
+
}
|
|
2565
|
+
let isFirstLine = true;
|
|
2566
|
+
finalInputStream.pipe(csvStream).on("data", (row) => {
|
|
2567
|
+
if (isFirstLine) {
|
|
2568
|
+
let headers = [];
|
|
2569
|
+
if (isGeneratorConfigArray) {
|
|
2570
|
+
headers = Object.values(row).filter((r) => !!r);
|
|
2571
|
+
} else {
|
|
2572
|
+
headers = Object.keys(row).filter((r) => !!r);
|
|
2573
|
+
}
|
|
2574
|
+
console.debug(`${logPrefix} CSV Detected headers:`, headers.map((h, idx) => {
|
|
2575
|
+
const info = getStringHexInfo(h);
|
|
2576
|
+
return `[${idx}]: "${info.original}" [${info.hex}]`;
|
|
2577
|
+
}));
|
|
2578
|
+
if (isGeneratorConfigArray) {
|
|
2579
|
+
colsWithParsingConfig.forEach((col, i) => {
|
|
2580
|
+
if (col !== headers[i]) {
|
|
2581
|
+
const hexComparison = formatHexComparison(col, headers[i] || "<undefined>");
|
|
2582
|
+
console.error(`${logPrefix} CSV Header Mismatch:${hexComparison}`);
|
|
2583
|
+
throw new Error(format4(
|
|
2584
|
+
`CSV header mismatch at position %s: expected '%s' but got '%s' in file %s`,
|
|
2585
|
+
i,
|
|
2586
|
+
col,
|
|
2587
|
+
headers[i] || "<undefined>",
|
|
2588
|
+
path2
|
|
2589
|
+
));
|
|
2590
|
+
}
|
|
2591
|
+
});
|
|
2592
|
+
} else {
|
|
2593
|
+
colsWithParsingConfig.forEach((col) => {
|
|
2594
|
+
if (!headers.includes(col)) {
|
|
2595
|
+
const colInfo = getStringHexInfo(col);
|
|
2596
|
+
console.error(`CSV Missing column: "${col}" [${colInfo.hex}]`);
|
|
2597
|
+
console.error(`Available headers:`, headers.map((h) => `"${h}" [${getStringHexInfo(h).hex}]`));
|
|
2598
|
+
const trimmedMatch = headers.find((h) => h.trim() === col.trim());
|
|
2599
|
+
if (trimmedMatch) {
|
|
2600
|
+
console.error(`Potential trimmed match: "${trimmedMatch}" [${getStringHexInfo(trimmedMatch).hex}]`);
|
|
2601
|
+
}
|
|
2602
|
+
throw new Error(format4(
|
|
2603
|
+
`CSV missing expected column '%s' in file: %s`,
|
|
2604
|
+
col,
|
|
2605
|
+
path2
|
|
2606
|
+
));
|
|
2607
|
+
}
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
isFirstLine = false;
|
|
2611
|
+
} else {
|
|
2612
|
+
initialReadStream.destroy();
|
|
2613
|
+
}
|
|
2614
|
+
}).on("error", (error) => {
|
|
2615
|
+
reject(new Error(`Error processing CSV file: ${error.message}`));
|
|
2616
|
+
});
|
|
2617
|
+
initialReadStream.on("close", () => {
|
|
2618
|
+
resolve();
|
|
2619
|
+
});
|
|
2620
|
+
});
|
|
2621
|
+
};
|
|
2622
|
+
|
|
2623
|
+
// src/sdk/file-processing/csv/writer.ts
|
|
2624
|
+
import { createObjectCsvWriter } from "csv-writer";
|
|
2625
|
+
import { format as format5 } from "util";
|
|
2626
|
+
var logPrefix2 = "[csvWriter]";
|
|
2627
|
+
var writeDataToCsv = async (fileName, data) => {
|
|
2628
|
+
try {
|
|
2629
|
+
const firstRow = data[0];
|
|
2630
|
+
if (!firstRow) {
|
|
2631
|
+
console.log(`${logPrefix2} No data to write to CSV file=%s`, fileName);
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
const csvWriter = createObjectCsvWriter({
|
|
2635
|
+
path: fileName,
|
|
2636
|
+
header: Object.keys(firstRow).map((col) => {
|
|
2637
|
+
return { id: col, title: col };
|
|
2638
|
+
})
|
|
2639
|
+
});
|
|
2640
|
+
await csvWriter.writeRecords(data);
|
|
2641
|
+
console.log(`${logPrefix2} CSV file=%s written successfully`, fileName);
|
|
2642
|
+
} catch (err) {
|
|
2643
|
+
if (err instanceof Error) {
|
|
2644
|
+
throw new Error(format5(`error while writing csv file=%s, err: %s`, fileName, err.message));
|
|
2645
|
+
}
|
|
2646
|
+
throw err;
|
|
2647
|
+
}
|
|
2648
|
+
};
|
|
2649
|
+
var writeGeneratorToCSV = async (generator, outputFileName) => {
|
|
2650
|
+
try {
|
|
2651
|
+
let rowCount = 0;
|
|
2652
|
+
const firstDataRow = (await generator.next()).value;
|
|
2653
|
+
if (!firstDataRow) {
|
|
2654
|
+
console.log(`${logPrefix2} [%s] first data row of csv empty, skipping`, outputFileName);
|
|
2655
|
+
return 0;
|
|
2656
|
+
}
|
|
2657
|
+
console.log(`${logPrefix2} [%s] inferring headers from first data row`, outputFileName);
|
|
2658
|
+
const headers = Object.keys(firstDataRow).map((col) => {
|
|
2659
|
+
return {
|
|
2660
|
+
id: col,
|
|
2661
|
+
title: col
|
|
2662
|
+
};
|
|
2663
|
+
});
|
|
2664
|
+
const csvWriter = createObjectCsvWriter({
|
|
2665
|
+
path: outputFileName,
|
|
2666
|
+
header: headers,
|
|
2667
|
+
alwaysQuote: true
|
|
2668
|
+
});
|
|
2669
|
+
console.log(`${logPrefix2} [%s] writing csv`, outputFileName);
|
|
2670
|
+
let batch = [firstDataRow];
|
|
2671
|
+
for await (const data of generator) {
|
|
2672
|
+
batch.push(data);
|
|
2673
|
+
rowCount++;
|
|
2674
|
+
if (batch.length > 1e4) {
|
|
2675
|
+
try {
|
|
2676
|
+
await csvWriter.writeRecords(batch);
|
|
2677
|
+
} catch (err) {
|
|
2678
|
+
if (err instanceof Error) {
|
|
2679
|
+
throw new Error(
|
|
2680
|
+
format5(
|
|
2681
|
+
`error while writing batch to csv batch (first 3 records)=%j, err: %s`,
|
|
2682
|
+
batch.slice(0, 3),
|
|
2683
|
+
err.message
|
|
2684
|
+
)
|
|
2685
|
+
);
|
|
2686
|
+
}
|
|
2687
|
+
throw err;
|
|
2688
|
+
}
|
|
2689
|
+
batch = [];
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
await csvWriter.writeRecords(batch);
|
|
2693
|
+
console.log(`${logPrefix2} [%s] csv written successfully with %d rows`, outputFileName, rowCount);
|
|
2694
|
+
return rowCount;
|
|
2695
|
+
} catch (err) {
|
|
2696
|
+
if (err instanceof Error) {
|
|
2697
|
+
throw new Error(format5(`error while writing csv file=%s, err: %s`, outputFileName, err.message));
|
|
2698
|
+
}
|
|
2699
|
+
throw err;
|
|
2700
|
+
}
|
|
2701
|
+
};
|
|
2702
|
+
|
|
2703
|
+
// src/sdk/file-processing/file-stream.ts
|
|
2704
|
+
import XLSX2 from "xlsx";
|
|
2705
|
+
var FileStream = class extends Stream {
|
|
2706
|
+
localFilePaths;
|
|
2707
|
+
constructor(config, localFilePath, tapState, target) {
|
|
2708
|
+
super(config, tapState, target);
|
|
2709
|
+
this.localFilePaths = Array.isArray(localFilePath) ? localFilePath : [localFilePath];
|
|
2710
|
+
this.streamId = config.streamId;
|
|
2711
|
+
this.primaryKey = config.primaryKeys;
|
|
2712
|
+
this.replicationMethod = config.replicationMethod;
|
|
2713
|
+
}
|
|
2714
|
+
/**
|
|
2715
|
+
* Build JSON Schema from the file config's field definitions.
|
|
2716
|
+
*/
|
|
2717
|
+
async getSchema() {
|
|
2718
|
+
if (this.config.format === "EXCEL" /* EXCEL */) {
|
|
2719
|
+
const excelConfig = this.config.excel;
|
|
2720
|
+
if (excelConfig.type === "single-sheet-extraction") {
|
|
2721
|
+
const jsonSchema = excelFieldsToJsonSchema(excelConfig.columns);
|
|
2722
|
+
return { jsonSchema };
|
|
2723
|
+
}
|
|
2724
|
+
return void 0;
|
|
2725
|
+
}
|
|
2726
|
+
if (this.config.format === "CSV" /* CSV */) {
|
|
2727
|
+
const jsonSchema = csvFieldsToJsonSchema(this.config.csv.fields);
|
|
2728
|
+
return { jsonSchema };
|
|
2729
|
+
}
|
|
2730
|
+
throw new Error(`FileStream: No valid format config for stream ${this.streamId}`);
|
|
2731
|
+
}
|
|
2732
|
+
/**
|
|
2733
|
+
* Yield records from all file(s).
|
|
2734
|
+
* When multiple file paths are provided, records are yielded sequentially from each file.
|
|
2735
|
+
*/
|
|
2736
|
+
async *_getRecords() {
|
|
2737
|
+
for (const filePath of this.localFilePaths) {
|
|
2738
|
+
if (this.config.format === "EXCEL" /* EXCEL */) {
|
|
2739
|
+
const data = await extractExcelRows(filePath, this.config.excel);
|
|
2740
|
+
for (const row of data) {
|
|
2741
|
+
yield row;
|
|
2742
|
+
}
|
|
2743
|
+
} else if (this.config.format === "CSV" /* CSV */) {
|
|
2744
|
+
yield* rowGeneratorFromCsv(filePath, this.config.csv);
|
|
2745
|
+
} else {
|
|
2746
|
+
throw new Error(`FileStream: Unsupported format for stream ${this.streamId}`);
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
};
|
|
2751
|
+
function createExcelStreamConfig(excelConfig, fileName, localFilePath) {
|
|
2752
|
+
if (excelConfig.type === "single-sheet-extraction") {
|
|
2753
|
+
const primaryKeys = extractPrimaryKeysFromExcelFields(excelConfig.columns);
|
|
2754
|
+
let tableName2;
|
|
2755
|
+
if (typeof excelConfig.tableName === "function") {
|
|
2756
|
+
const variables = excelConfig.fileNameVariablesExtractor(fileName);
|
|
2757
|
+
tableName2 = excelConfig.tableName(fileName, void 0, variables);
|
|
2758
|
+
} else {
|
|
2759
|
+
tableName2 = excelConfig.tableName;
|
|
2760
|
+
}
|
|
2761
|
+
return {
|
|
2762
|
+
format: "EXCEL" /* EXCEL */,
|
|
2763
|
+
streamId: tableName2,
|
|
2764
|
+
replicationMethod: excelConfig.replicationMethod,
|
|
2765
|
+
primaryKeys,
|
|
2766
|
+
excel: excelConfig
|
|
2767
|
+
};
|
|
2768
|
+
}
|
|
2769
|
+
let tableName;
|
|
2770
|
+
if (typeof excelConfig.tableName === "function") {
|
|
2771
|
+
if (!localFilePath) {
|
|
2772
|
+
throw new Error("localFilePath is required for processor configs with dynamic table names");
|
|
2773
|
+
}
|
|
2774
|
+
const workbook = XLSX2.readFile(localFilePath, { dense: true });
|
|
2775
|
+
const variables = excelConfig.fileNameVariablesExtractor(fileName, workbook);
|
|
2776
|
+
tableName = excelConfig.tableName(fileName, workbook, variables);
|
|
2777
|
+
} else {
|
|
2778
|
+
tableName = excelConfig.tableName;
|
|
2779
|
+
}
|
|
2780
|
+
return {
|
|
2781
|
+
format: "EXCEL" /* EXCEL */,
|
|
2782
|
+
streamId: tableName,
|
|
2783
|
+
replicationMethod: excelConfig.replicationMethod,
|
|
2784
|
+
primaryKeys: [],
|
|
2785
|
+
excel: excelConfig
|
|
2786
|
+
};
|
|
2787
|
+
}
|
|
2788
|
+
function createCsvStreamConfig(streamId, csvConfig, options) {
|
|
2789
|
+
return {
|
|
2790
|
+
format: "CSV" /* CSV */,
|
|
2791
|
+
streamId,
|
|
2792
|
+
replicationMethod: options?.replicationMethod ?? "FULL_TABLE" /* FULL_TABLE */,
|
|
2793
|
+
primaryKeys: options?.primaryKeys ?? extractPrimaryKeysFromCsvConfig(csvConfig.fields),
|
|
2794
|
+
csv: csvConfig
|
|
2795
|
+
};
|
|
2796
|
+
}
|
|
2797
|
+
async function processFileStreams(entries, tapState, target) {
|
|
2798
|
+
for (const entry of entries) {
|
|
2799
|
+
const stream = new FileStream(entry.config, entry.filePath, tapState, target);
|
|
2800
|
+
await stream.sync();
|
|
2801
|
+
}
|
|
2802
|
+
await target.complete();
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
// src/sdk/file-processing/csv/config-builder.ts
|
|
2806
|
+
var CsvExtractionConfigBuilder = class _CsvExtractionConfigBuilder {
|
|
2807
|
+
_separator = ",";
|
|
2808
|
+
_encoding;
|
|
2809
|
+
_addSyncedAtColumn;
|
|
2810
|
+
_fields;
|
|
2811
|
+
_fieldsMode;
|
|
2812
|
+
_replicationMethod;
|
|
2813
|
+
_primaryKeys;
|
|
2814
|
+
static create() {
|
|
2815
|
+
return new _CsvExtractionConfigBuilder();
|
|
2816
|
+
}
|
|
2817
|
+
separator(sep) {
|
|
2818
|
+
this._separator = sep;
|
|
2819
|
+
return this;
|
|
2820
|
+
}
|
|
2821
|
+
encoding(enc) {
|
|
2822
|
+
this._encoding = enc;
|
|
2823
|
+
return this;
|
|
2824
|
+
}
|
|
2825
|
+
addSyncedAtColumn(enabled = true) {
|
|
2826
|
+
this._addSyncedAtColumn = enabled;
|
|
2827
|
+
return this;
|
|
2828
|
+
}
|
|
2829
|
+
fieldsFromDict(mapping) {
|
|
2830
|
+
if (this._fieldsMode === "array") {
|
|
2831
|
+
throw new Error("Cannot use fieldsFromDict after fieldsFromArray");
|
|
2832
|
+
}
|
|
2833
|
+
this._fieldsMode = "dict";
|
|
2834
|
+
this._fields = mapping;
|
|
2835
|
+
return this;
|
|
2836
|
+
}
|
|
2837
|
+
fieldsFromArray(fields) {
|
|
2838
|
+
if (this._fieldsMode === "dict") {
|
|
2839
|
+
throw new Error("Cannot use fieldsFromArray after fieldsFromDict");
|
|
2840
|
+
}
|
|
2841
|
+
this._fieldsMode = "array";
|
|
2842
|
+
this._fields = fields;
|
|
2843
|
+
return this;
|
|
2844
|
+
}
|
|
2845
|
+
replicationMethod(method) {
|
|
2846
|
+
this._replicationMethod = method;
|
|
2847
|
+
return this;
|
|
2848
|
+
}
|
|
2849
|
+
primaryKeys(keys) {
|
|
2850
|
+
this._primaryKeys = keys;
|
|
2851
|
+
return this;
|
|
2852
|
+
}
|
|
2853
|
+
build() {
|
|
2854
|
+
if (!this._fields) {
|
|
2855
|
+
throw new Error("Fields must be configured (use fieldsFromDict or fieldsFromArray)");
|
|
2856
|
+
}
|
|
2857
|
+
if (Array.isArray(this._fields) && this._fields.length === 0) {
|
|
2858
|
+
throw new Error("Fields must not be empty");
|
|
2859
|
+
}
|
|
2860
|
+
if (!Array.isArray(this._fields) && Object.keys(this._fields).length === 0) {
|
|
2861
|
+
throw new Error("Fields must not be empty");
|
|
2862
|
+
}
|
|
2863
|
+
const config = {
|
|
2864
|
+
separator: this._separator,
|
|
2865
|
+
fields: this._fields
|
|
2866
|
+
};
|
|
2867
|
+
if (this._encoding !== void 0) {
|
|
2868
|
+
config.encoding = this._encoding;
|
|
2869
|
+
}
|
|
2870
|
+
if (this._addSyncedAtColumn !== void 0) {
|
|
2871
|
+
config.addSyncedAtColumn = this._addSyncedAtColumn;
|
|
2872
|
+
}
|
|
2873
|
+
return config;
|
|
2874
|
+
}
|
|
2875
|
+
buildStreamConfig(streamId) {
|
|
2876
|
+
const csvConfig = this.build();
|
|
2877
|
+
const options = {};
|
|
2878
|
+
if (this._replicationMethod !== void 0) {
|
|
2879
|
+
options.replicationMethod = this._replicationMethod;
|
|
2880
|
+
}
|
|
2881
|
+
if (this._primaryKeys !== void 0) {
|
|
2882
|
+
options.primaryKeys = this._primaryKeys;
|
|
2883
|
+
}
|
|
2884
|
+
return createCsvStreamConfig(streamId, csvConfig, options);
|
|
2885
|
+
}
|
|
2886
|
+
};
|
|
2887
|
+
|
|
2888
|
+
// src/sdk/file-processing/file-tap.ts
|
|
2889
|
+
var FileTap = class _FileTap extends Tap {
|
|
2890
|
+
entries;
|
|
2891
|
+
constructor(target, config, stateProvider, entries) {
|
|
2892
|
+
super(target, config, stateProvider);
|
|
2893
|
+
this.entries = entries;
|
|
2894
|
+
}
|
|
2895
|
+
/**
|
|
2896
|
+
* Convenience constructor for the common case where all configs share the same file path.
|
|
2897
|
+
*/
|
|
2898
|
+
static fromConfigs(target, config, stateProvider, fileConfigs, sharedFilePath) {
|
|
2899
|
+
const entries = fileConfigs.map((c) => ({
|
|
2900
|
+
config: c,
|
|
2901
|
+
filePath: sharedFilePath
|
|
2902
|
+
}));
|
|
2903
|
+
return new _FileTap(target, config, stateProvider, entries);
|
|
2904
|
+
}
|
|
2905
|
+
async init() {
|
|
2906
|
+
for (const entry of this.entries) {
|
|
2907
|
+
const stream = new FileStream(
|
|
2908
|
+
entry.config,
|
|
2909
|
+
entry.filePath,
|
|
2910
|
+
this.tapState,
|
|
2911
|
+
this.target
|
|
2912
|
+
);
|
|
2913
|
+
this.streams.push(stream);
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
};
|
|
2917
|
+
|
|
2918
|
+
// src/sdk/file-processing/file-patterns.ts
|
|
2919
|
+
var VariableExtractors = class {
|
|
2920
|
+
/**
|
|
2921
|
+
* Returns the filename as a variable.
|
|
2922
|
+
*/
|
|
2923
|
+
static filename() {
|
|
2924
|
+
return (filename) => ({
|
|
2925
|
+
fileName: filename
|
|
2926
|
+
});
|
|
2927
|
+
}
|
|
2928
|
+
/**
|
|
2929
|
+
* Extracts named capture groups from a regex pattern.
|
|
2930
|
+
*/
|
|
2931
|
+
static regex(pattern) {
|
|
2932
|
+
return (filename) => {
|
|
2933
|
+
const match = filename.match(pattern);
|
|
2934
|
+
return match?.groups ?? {};
|
|
2935
|
+
};
|
|
2936
|
+
}
|
|
2937
|
+
/**
|
|
2938
|
+
* Combines multiple extraction functions.
|
|
2939
|
+
*/
|
|
2940
|
+
static combine(...extractors) {
|
|
2941
|
+
return (filename) => {
|
|
2942
|
+
return extractors.reduce((acc, extractor) => {
|
|
2943
|
+
return { ...acc, ...extractor(filename) };
|
|
2944
|
+
}, {});
|
|
2945
|
+
};
|
|
2946
|
+
}
|
|
2947
|
+
};
|
|
2948
|
+
var FilePatterns = class {
|
|
2949
|
+
/**
|
|
2950
|
+
* Creates a validator for files starting with a specific prefix (case-insensitive).
|
|
2951
|
+
*/
|
|
2952
|
+
static startsWith(prefix) {
|
|
2953
|
+
return (filename) => filename.toLowerCase().startsWith(prefix.toLowerCase());
|
|
2954
|
+
}
|
|
2955
|
+
/**
|
|
2956
|
+
* Creates a validator for files matching a regex pattern.
|
|
2957
|
+
*/
|
|
2958
|
+
static regex(pattern) {
|
|
2959
|
+
return (filename) => pattern.test(filename);
|
|
2960
|
+
}
|
|
2961
|
+
/**
|
|
2962
|
+
* Combines multiple validators with AND logic.
|
|
2963
|
+
*/
|
|
2964
|
+
static and(...validators) {
|
|
2965
|
+
return (filename) => validators.every((validator) => validator(filename));
|
|
2966
|
+
}
|
|
2967
|
+
/**
|
|
2968
|
+
* Combines multiple validators with OR logic.
|
|
2969
|
+
*/
|
|
2970
|
+
static or(...validators) {
|
|
2971
|
+
return (filename) => validators.some((validator) => validator(filename));
|
|
2972
|
+
}
|
|
2973
|
+
};
|
|
2974
|
+
|
|
2975
|
+
// src/services/sftp.ts
|
|
2976
|
+
import Client from "ssh2-sftp-client";
|
|
2977
|
+
import { default as default2 } from "ssh2-sftp-client";
|
|
2978
|
+
var SftpService = new Client();
|
|
2979
|
+
|
|
2980
|
+
// src/services/cloud-storage.ts
|
|
2981
|
+
import { Storage } from "@google-cloud/storage";
|
|
2982
|
+
import { format as format6 } from "util";
|
|
2983
|
+
import * as pathModule from "path";
|
|
2984
|
+
import { existsSync, mkdirSync, unlinkSync as unlinkSync2 } from "fs";
|
|
2985
|
+
|
|
2986
|
+
// node_modules/uuid/dist/esm-node/rng.js
|
|
2987
|
+
import crypto from "crypto";
|
|
2988
|
+
var rnds8Pool = new Uint8Array(256);
|
|
2989
|
+
var poolPtr = rnds8Pool.length;
|
|
2990
|
+
function rng() {
|
|
2991
|
+
if (poolPtr > rnds8Pool.length - 16) {
|
|
2992
|
+
crypto.randomFillSync(rnds8Pool);
|
|
2993
|
+
poolPtr = 0;
|
|
2994
|
+
}
|
|
2995
|
+
return rnds8Pool.slice(poolPtr, poolPtr += 16);
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
// node_modules/uuid/dist/esm-node/regex.js
|
|
2999
|
+
var regex_default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;
|
|
3000
|
+
|
|
3001
|
+
// node_modules/uuid/dist/esm-node/validate.js
|
|
3002
|
+
function validate(uuid) {
|
|
3003
|
+
return typeof uuid === "string" && regex_default.test(uuid);
|
|
3004
|
+
}
|
|
3005
|
+
var validate_default = validate;
|
|
3006
|
+
|
|
3007
|
+
// node_modules/uuid/dist/esm-node/stringify.js
|
|
3008
|
+
var byteToHex = [];
|
|
3009
|
+
for (let i = 0; i < 256; ++i) {
|
|
3010
|
+
byteToHex.push((i + 256).toString(16).substr(1));
|
|
3011
|
+
}
|
|
3012
|
+
function stringify3(arr, offset = 0) {
|
|
3013
|
+
const uuid = (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
|
|
3014
|
+
if (!validate_default(uuid)) {
|
|
3015
|
+
throw TypeError("Stringified UUID is invalid");
|
|
3016
|
+
}
|
|
3017
|
+
return uuid;
|
|
3018
|
+
}
|
|
3019
|
+
var stringify_default = stringify3;
|
|
3020
|
+
|
|
3021
|
+
// node_modules/uuid/dist/esm-node/v4.js
|
|
3022
|
+
function v4(options, buf, offset) {
|
|
3023
|
+
options = options || {};
|
|
3024
|
+
const rnds = options.random || (options.rng || rng)();
|
|
3025
|
+
rnds[6] = rnds[6] & 15 | 64;
|
|
3026
|
+
rnds[8] = rnds[8] & 63 | 128;
|
|
3027
|
+
if (buf) {
|
|
3028
|
+
offset = offset || 0;
|
|
3029
|
+
for (let i = 0; i < 16; ++i) {
|
|
3030
|
+
buf[offset + i] = rnds[i];
|
|
3031
|
+
}
|
|
3032
|
+
return buf;
|
|
3033
|
+
}
|
|
3034
|
+
return stringify_default(rnds);
|
|
3035
|
+
}
|
|
3036
|
+
var v4_default = v4;
|
|
3037
|
+
|
|
3038
|
+
// src/services/cloud-storage.ts
|
|
3039
|
+
var logPrefix3 = "[CloudStorageService]";
|
|
3040
|
+
var tmpDir = "tmp";
|
|
3041
|
+
var CloudStorageService = class {
|
|
3042
|
+
storage;
|
|
3043
|
+
bucket;
|
|
3044
|
+
processedSuffix;
|
|
3045
|
+
supportedExtensions;
|
|
3046
|
+
path;
|
|
3047
|
+
constructor(bucketName, path2, opts) {
|
|
3048
|
+
this.storage = new Storage({ retryOptions: { autoRetry: true, maxRetries: 20 } });
|
|
3049
|
+
this.bucket = this.storage.bucket(bucketName);
|
|
3050
|
+
this.path = path2;
|
|
3051
|
+
this.processedSuffix = opts?.processedSuffix ?? ".processed";
|
|
3052
|
+
this.supportedExtensions = opts?.supportedExtensions ?? [];
|
|
3053
|
+
}
|
|
3054
|
+
/**
|
|
3055
|
+
* Lists all files in the bucket with an optional prefix.
|
|
3056
|
+
*/
|
|
3057
|
+
async listFiles(prefix) {
|
|
3058
|
+
const effectivePrefix = prefix || this.path;
|
|
3059
|
+
const [files] = await this.bucket.getFiles(
|
|
3060
|
+
effectivePrefix ? { prefix: effectivePrefix } : void 0
|
|
3061
|
+
);
|
|
3062
|
+
return files.map((f) => f.name);
|
|
3063
|
+
}
|
|
3064
|
+
/**
|
|
3065
|
+
* Returns files that haven't been marked as processed.
|
|
3066
|
+
* Filters by supported extensions if configured.
|
|
3067
|
+
*/
|
|
3068
|
+
async getUnprocessedFiles() {
|
|
3069
|
+
const allFiles = await this.listFiles();
|
|
3070
|
+
const markerFiles = new Set(
|
|
3071
|
+
allFiles.filter((f) => f.endsWith(this.processedSuffix)).map((f) => f.replace(this.processedSuffix, ""))
|
|
3072
|
+
);
|
|
3073
|
+
return allFiles.filter((f) => {
|
|
3074
|
+
if (f.endsWith(this.processedSuffix)) return false;
|
|
3075
|
+
if (f.endsWith("/")) return false;
|
|
3076
|
+
if (this.supportedExtensions.length > 0) {
|
|
3077
|
+
const ext = pathModule.extname(f).toLowerCase();
|
|
3078
|
+
if (!this.supportedExtensions.includes(ext)) return false;
|
|
3079
|
+
}
|
|
3080
|
+
return !markerFiles.has(f);
|
|
3081
|
+
});
|
|
3082
|
+
}
|
|
3083
|
+
/**
|
|
3084
|
+
* Creates a marker file to indicate a file has been processed.
|
|
3085
|
+
*/
|
|
3086
|
+
async createMarkerFile(fileName) {
|
|
3087
|
+
const markerFileName = `${fileName}${this.processedSuffix}`;
|
|
3088
|
+
try {
|
|
3089
|
+
const file = this.bucket.file(markerFileName);
|
|
3090
|
+
await file.save(format6("Marked file %s as processed", fileName));
|
|
3091
|
+
logger.info(`${logPrefix3} Marker file ${markerFileName} created successfully.`);
|
|
3092
|
+
} catch (err) {
|
|
3093
|
+
if (err instanceof Error) {
|
|
3094
|
+
throw new Error(format6(`error while writing marker file=%s, err:%s`, markerFileName, err.message));
|
|
3095
|
+
}
|
|
3096
|
+
throw err;
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
/**
|
|
3100
|
+
* Downloads a file from GCS to a local tmp directory.
|
|
3101
|
+
* Returns the local path of the downloaded file.
|
|
3102
|
+
*/
|
|
3103
|
+
async downloadFile(filePath, fileName) {
|
|
3104
|
+
const file = this.bucket.file(filePath);
|
|
3105
|
+
const tmpDirPath = pathModule.join(process.cwd(), tmpDir);
|
|
3106
|
+
if (!existsSync(tmpDirPath)) {
|
|
3107
|
+
mkdirSync(tmpDirPath);
|
|
3108
|
+
}
|
|
3109
|
+
const destFilename = pathModule.join(process.cwd(), tmpDir, fileName);
|
|
3110
|
+
try {
|
|
3111
|
+
if (existsSync(destFilename)) {
|
|
3112
|
+
unlinkSync2(destFilename);
|
|
3113
|
+
}
|
|
3114
|
+
await file.download({ destination: destFilename });
|
|
3115
|
+
logger.info(`${logPrefix3} Downloaded ${fileName} to ${destFilename}`);
|
|
3116
|
+
return destFilename;
|
|
3117
|
+
} catch (err) {
|
|
3118
|
+
if (err instanceof Error) {
|
|
3119
|
+
throw new Error(format6(
|
|
3120
|
+
`can't download file=%s from bucket, err:%s`,
|
|
3121
|
+
fileName,
|
|
3122
|
+
err.message
|
|
3123
|
+
));
|
|
3124
|
+
}
|
|
3125
|
+
throw err;
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
/**
|
|
3129
|
+
* Uploads a local file to GCS.
|
|
3130
|
+
*/
|
|
3131
|
+
async uploadFile(localPath, destPath) {
|
|
3132
|
+
try {
|
|
3133
|
+
logger.info(`${logPrefix3} preparing to upload '%s' to '%s'`, localPath, destPath);
|
|
3134
|
+
const [file] = await this.bucket.upload(localPath, { destination: destPath });
|
|
3135
|
+
logger.info(`${logPrefix3} file %s has been successfully uploaded into %s`, localPath, destPath);
|
|
3136
|
+
return file;
|
|
3137
|
+
} catch (err) {
|
|
3138
|
+
if (err instanceof Error) {
|
|
3139
|
+
throw new Error(format6(`error while uploading file file=%s into path=%s, err:%s`, localPath, destPath, err.message));
|
|
3140
|
+
}
|
|
3141
|
+
throw err;
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
/**
|
|
3145
|
+
* Reads a GCS object and returns its contents as a UTF-8 string.
|
|
3146
|
+
*/
|
|
3147
|
+
async readObjectAsString(objectPath) {
|
|
3148
|
+
try {
|
|
3149
|
+
await this.bucket.get({ autoCreate: false });
|
|
3150
|
+
const fileRef = this.bucket.file(objectPath);
|
|
3151
|
+
const [exists] = await fileRef.exists();
|
|
3152
|
+
if (!exists) {
|
|
3153
|
+
throw new Error(`GCS object not found: gs://${this.bucket.name}/${objectPath}`);
|
|
3154
|
+
}
|
|
3155
|
+
const [contents] = await fileRef.download();
|
|
3156
|
+
return contents.toString("utf8");
|
|
3157
|
+
} catch (err) {
|
|
3158
|
+
throw new Error(format6(`error reading GCS object gs://${this.bucket.name}/${objectPath}, err: %s`, err?.message));
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
/**
|
|
3162
|
+
* Writes a string to a GCS object with JSON content type.
|
|
3163
|
+
*/
|
|
3164
|
+
async writeStringObject(objectPath, contents) {
|
|
3165
|
+
try {
|
|
3166
|
+
await this.bucket.get({ autoCreate: false });
|
|
3167
|
+
const fileRef = this.bucket.file(objectPath);
|
|
3168
|
+
await fileRef.save(contents, { contentType: "application/json" });
|
|
3169
|
+
logger.info(`Uploaded object to gs://${this.bucket.name}/${objectPath}`);
|
|
3170
|
+
} catch (err) {
|
|
3171
|
+
throw new Error(format6(`error writing GCS object gs://${this.bucket.name}/${objectPath}, err: %s`, err?.message));
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
/**
|
|
3175
|
+
* Uploads a local file to the bucket with a unique name based on prefix, streamId, and UUID.
|
|
3176
|
+
* Returns the GCS File reference.
|
|
3177
|
+
*/
|
|
3178
|
+
async uploadFileWithUniqueName(filePath, prefix, streamId) {
|
|
3179
|
+
try {
|
|
3180
|
+
await this.bucket.get({ autoCreate: false });
|
|
3181
|
+
const destinationFileName = `${prefix}/${streamId}-${v4_default()}.jsonnl`;
|
|
3182
|
+
await this.bucket.upload(filePath, {
|
|
3183
|
+
destination: destinationFileName
|
|
3184
|
+
});
|
|
3185
|
+
logger.info(`Uploaded ${filePath} into ${this.bucket.name}/${destinationFileName} GCS File`);
|
|
3186
|
+
return this.bucket.file(destinationFileName);
|
|
3187
|
+
} catch (err) {
|
|
3188
|
+
logger.error(`Issue when uploading file into GCS bucket for stream: ${streamId}
|
|
3189
|
+
|
|
3190
|
+
Error: ${err.message}
|
|
3191
|
+
Stack: ${err.stack}
|
|
3192
|
+
Code: ${err.code}
|
|
3193
|
+
`);
|
|
3194
|
+
throw err;
|
|
2437
3195
|
}
|
|
2438
|
-
});
|
|
2439
|
-
returnString = returnString.replace(/^[-\d\s]*/g, "");
|
|
2440
|
-
returnString = returnString.toLowerCase();
|
|
2441
|
-
returnString = returnString.replace("`", "");
|
|
2442
|
-
returnString = returnString.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
2443
|
-
if (returnString.length > 300) {
|
|
2444
|
-
returnString = returnString.substr(0, 300);
|
|
2445
3196
|
}
|
|
2446
|
-
|
|
2447
|
-
|
|
3197
|
+
};
|
|
3198
|
+
|
|
3199
|
+
// src/services/zip.ts
|
|
3200
|
+
import * as fse from "fs-extra";
|
|
3201
|
+
import decompress from "decompress";
|
|
3202
|
+
var unzip = async (zipFilePath, extractedPath) => {
|
|
3203
|
+
await fse.ensureDir(extractedPath);
|
|
3204
|
+
await decompress(zipFilePath, extractedPath);
|
|
3205
|
+
return extractedPath;
|
|
3206
|
+
};
|
|
3207
|
+
|
|
3208
|
+
// src/targets/bigquery/helpers.ts
|
|
3209
|
+
var safeColumnName = (key) => {
|
|
3210
|
+
let returnString = key;
|
|
3211
|
+
const shouldNotStartWith = ["_TABLE_", "_FILE_", "_PARTITION"];
|
|
3212
|
+
shouldNotStartWith.forEach((snm) => {
|
|
3213
|
+
if (returnString.startsWith(snm)) {
|
|
3214
|
+
returnString = returnString.replace(snm, "");
|
|
3215
|
+
}
|
|
3216
|
+
});
|
|
3217
|
+
returnString = returnString.replace(/^[-\d\s]*/g, "");
|
|
3218
|
+
returnString = returnString.toLowerCase();
|
|
3219
|
+
returnString = returnString.replace("`", "");
|
|
3220
|
+
returnString = returnString.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
3221
|
+
if (returnString.length > 300) {
|
|
3222
|
+
returnString = returnString.substr(0, 300);
|
|
3223
|
+
}
|
|
3224
|
+
const pattern = /[^a-zA-Z0-9_]/gm;
|
|
3225
|
+
return returnString.replace(pattern, "_");
|
|
2448
3226
|
};
|
|
2449
3227
|
var safeTableName = (key) => {
|
|
2450
3228
|
let returnString = key;
|
|
@@ -2460,15 +3238,15 @@ var safeTableName = (key) => {
|
|
|
2460
3238
|
|
|
2461
3239
|
// src/targets/bigquery/service/record.ts
|
|
2462
3240
|
import Decimal from "decimal.js";
|
|
2463
|
-
import
|
|
3241
|
+
import dayjs5 from "dayjs";
|
|
2464
3242
|
var validateDateRange = (record, schema) => {
|
|
2465
3243
|
Object.keys(schema).forEach((key) => {
|
|
2466
|
-
const { format:
|
|
2467
|
-
if (
|
|
3244
|
+
const { format: format8 } = schema[key];
|
|
3245
|
+
if (format8 === "date-time") {
|
|
2468
3246
|
if (record[key]) {
|
|
2469
|
-
const formattedDate =
|
|
2470
|
-
const isNotTooMuchInTheFuture =
|
|
2471
|
-
const isNotTooMuchInThePast =
|
|
3247
|
+
const formattedDate = dayjs5(record[key]).format(defaultDateTimeFormat);
|
|
3248
|
+
const isNotTooMuchInTheFuture = dayjs5(record[key]).isBefore(dayjs5("9999-12-31 23:59:59.999999"));
|
|
3249
|
+
const isNotTooMuchInThePast = dayjs5(record[key]).isAfter(dayjs5("0001-01-01 00:00:00"));
|
|
2472
3250
|
if (formattedDate !== "Invalid date" && isNotTooMuchInTheFuture && isNotTooMuchInThePast) {
|
|
2473
3251
|
record[key] = formattedDate;
|
|
2474
3252
|
} else {
|
|
@@ -2523,7 +3301,7 @@ var convertNumberIntoDecimal = (record, schema) => {
|
|
|
2523
3301
|
};
|
|
2524
3302
|
|
|
2525
3303
|
// src/targets/bigquery/service/dbSync.ts
|
|
2526
|
-
import
|
|
3304
|
+
import dayjs6 from "dayjs";
|
|
2527
3305
|
|
|
2528
3306
|
// src/targets/bigquery/service/bigquery.ts
|
|
2529
3307
|
import { BigQuery } from "@google-cloud/bigquery";
|
|
@@ -2548,96 +3326,6 @@ var BqClientHolder = class {
|
|
|
2548
3326
|
}
|
|
2549
3327
|
};
|
|
2550
3328
|
|
|
2551
|
-
// src/targets/bigquery/service/cloudStorage.ts
|
|
2552
|
-
import { Storage } from "@google-cloud/storage";
|
|
2553
|
-
|
|
2554
|
-
// node_modules/uuid/dist/esm-node/rng.js
|
|
2555
|
-
import crypto from "crypto";
|
|
2556
|
-
var rnds8Pool = new Uint8Array(256);
|
|
2557
|
-
var poolPtr = rnds8Pool.length;
|
|
2558
|
-
function rng() {
|
|
2559
|
-
if (poolPtr > rnds8Pool.length - 16) {
|
|
2560
|
-
crypto.randomFillSync(rnds8Pool);
|
|
2561
|
-
poolPtr = 0;
|
|
2562
|
-
}
|
|
2563
|
-
return rnds8Pool.slice(poolPtr, poolPtr += 16);
|
|
2564
|
-
}
|
|
2565
|
-
|
|
2566
|
-
// node_modules/uuid/dist/esm-node/regex.js
|
|
2567
|
-
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;
|
|
2568
|
-
|
|
2569
|
-
// node_modules/uuid/dist/esm-node/validate.js
|
|
2570
|
-
function validate(uuid) {
|
|
2571
|
-
return typeof uuid === "string" && regex_default.test(uuid);
|
|
2572
|
-
}
|
|
2573
|
-
var validate_default = validate;
|
|
2574
|
-
|
|
2575
|
-
// node_modules/uuid/dist/esm-node/stringify.js
|
|
2576
|
-
var byteToHex = [];
|
|
2577
|
-
for (let i = 0; i < 256; ++i) {
|
|
2578
|
-
byteToHex.push((i + 256).toString(16).substr(1));
|
|
2579
|
-
}
|
|
2580
|
-
function stringify3(arr, offset = 0) {
|
|
2581
|
-
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();
|
|
2582
|
-
if (!validate_default(uuid)) {
|
|
2583
|
-
throw TypeError("Stringified UUID is invalid");
|
|
2584
|
-
}
|
|
2585
|
-
return uuid;
|
|
2586
|
-
}
|
|
2587
|
-
var stringify_default = stringify3;
|
|
2588
|
-
|
|
2589
|
-
// node_modules/uuid/dist/esm-node/v4.js
|
|
2590
|
-
function v4(options, buf, offset) {
|
|
2591
|
-
options = options || {};
|
|
2592
|
-
const rnds = options.random || (options.rng || rng)();
|
|
2593
|
-
rnds[6] = rnds[6] & 15 | 64;
|
|
2594
|
-
rnds[8] = rnds[8] & 63 | 128;
|
|
2595
|
-
if (buf) {
|
|
2596
|
-
offset = offset || 0;
|
|
2597
|
-
for (let i = 0; i < 16; ++i) {
|
|
2598
|
-
buf[offset + i] = rnds[i];
|
|
2599
|
-
}
|
|
2600
|
-
return buf;
|
|
2601
|
-
}
|
|
2602
|
-
return stringify_default(rnds);
|
|
2603
|
-
}
|
|
2604
|
-
var v4_default = v4;
|
|
2605
|
-
|
|
2606
|
-
// src/targets/bigquery/service/cloudStorage.ts
|
|
2607
|
-
var uploadFileInBucket = async (streamId, gcsBuckerName, filePath) => {
|
|
2608
|
-
try {
|
|
2609
|
-
const retryOptions = { autoRetry: true, maxRetries: 20 };
|
|
2610
|
-
let storage = new Storage({ retryOptions });
|
|
2611
|
-
const bucketRef = storage.bucket(gcsBuckerName);
|
|
2612
|
-
await bucketRef.get({ autoCreate: false });
|
|
2613
|
-
const destinationFileName = `${streamId}-${v4_default()}.jsonnl`;
|
|
2614
|
-
await bucketRef.upload(filePath, {
|
|
2615
|
-
destination: destinationFileName
|
|
2616
|
-
});
|
|
2617
|
-
logger.info(`\u{1F5F3} Uploaded ${filePath} into ${gcsBuckerName}/${destinationFileName} GCS File`);
|
|
2618
|
-
return bucketRef.file(destinationFileName);
|
|
2619
|
-
} catch (err) {
|
|
2620
|
-
logger.error(`Issue when uploading file into GCS bucket for stream: ${streamId}
|
|
2621
|
-
|
|
2622
|
-
Error: ${err.message}
|
|
2623
|
-
Stack: ${err.stack}
|
|
2624
|
-
Code: ${err.code}
|
|
2625
|
-
`);
|
|
2626
|
-
if (err.code < 500) {
|
|
2627
|
-
await haltAndCatchFire(
|
|
2628
|
-
`unauthorized`,
|
|
2629
|
-
`We couldn't connect to your Cloud Storage bucket \u{1F614}
|
|
2630
|
-
|
|
2631
|
-
The error from Google is: '${err.message}' \u{1F440}
|
|
2632
|
-
|
|
2633
|
-
Could you troubleshoot your Cloud Storage configuration in Google Cloud and sync again the source? \u{1F64F}`,
|
|
2634
|
-
`Got error from cloud storage lib`
|
|
2635
|
-
);
|
|
2636
|
-
}
|
|
2637
|
-
throw err;
|
|
2638
|
-
}
|
|
2639
|
-
};
|
|
2640
|
-
|
|
2641
3329
|
// src/targets/bigquery/service/dbSync.ts
|
|
2642
3330
|
import chunk from "lodash/chunk.js";
|
|
2643
3331
|
import { Semaphore as Semaphore2 } from "async-mutex";
|
|
@@ -2678,21 +3366,21 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
|
|
|
2678
3366
|
safeColumnName = safeColumnName;
|
|
2679
3367
|
getWarehouseTypeFromJSONSchema = (definition) => {
|
|
2680
3368
|
const type = definition.type;
|
|
2681
|
-
const
|
|
3369
|
+
const format8 = definition.format;
|
|
2682
3370
|
let typeArr;
|
|
2683
3371
|
if (type instanceof Array) {
|
|
2684
3372
|
typeArr = type;
|
|
2685
3373
|
} else {
|
|
2686
3374
|
typeArr = [type];
|
|
2687
3375
|
}
|
|
2688
|
-
if (
|
|
2689
|
-
switch (
|
|
3376
|
+
if (format8) {
|
|
3377
|
+
switch (format8) {
|
|
2690
3378
|
case "date-time":
|
|
2691
3379
|
return "TIMESTAMP";
|
|
2692
3380
|
case "json":
|
|
2693
3381
|
return "STRING";
|
|
2694
3382
|
default:
|
|
2695
|
-
throw new Error(`StreamId: ${this.streamId} - Unsupported format: ${
|
|
3383
|
+
throw new Error(`StreamId: ${this.streamId} - Unsupported format: ${format8}`);
|
|
2696
3384
|
}
|
|
2697
3385
|
} else if (typeArr.includes("number")) {
|
|
2698
3386
|
return "NUMERIC";
|
|
@@ -2788,6 +3476,13 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
|
|
|
2788
3476
|
...this.getMergeQueries()
|
|
2789
3477
|
];
|
|
2790
3478
|
};
|
|
3479
|
+
getAppendQueries = () => {
|
|
3480
|
+
const columnUnsafeToSafeMapping = this.renamedColumnStore.getUnsafeToSafeColumnMapping(this.streamId);
|
|
3481
|
+
const columns = Object.keys(columnUnsafeToSafeMapping).map((unsafeKey) => `\`${columnUnsafeToSafeMapping[unsafeKey]}\``).join(`, `);
|
|
3482
|
+
return [
|
|
3483
|
+
`INSERT INTO ${this.sqlTableId} (${columns}) SELECT ${columns} FROM ${this.sqlStagingTableId};`
|
|
3484
|
+
];
|
|
3485
|
+
};
|
|
2791
3486
|
createDatabaseAndSchemaIfNotExists = async (retryCount) => {
|
|
2792
3487
|
try {
|
|
2793
3488
|
await semaphore2.runExclusive(async () => {
|
|
@@ -2892,7 +3587,7 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
|
|
|
2892
3587
|
await this.deleteStagingArea();
|
|
2893
3588
|
}
|
|
2894
3589
|
const schema = this.generateBigquerySchema();
|
|
2895
|
-
const expirationTime =
|
|
3590
|
+
const expirationTime = dayjs6().add(1, "day").valueOf().toString();
|
|
2896
3591
|
const createTableOptions = {
|
|
2897
3592
|
schema,
|
|
2898
3593
|
expirationTime
|
|
@@ -2912,10 +3607,11 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
|
|
|
2912
3607
|
};
|
|
2913
3608
|
loadStreamInStagingArea = async (localFilePath) => {
|
|
2914
3609
|
try {
|
|
2915
|
-
const
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
3610
|
+
const gcsService = new CloudStorageService(this.config.loading_deck_gcs_bucket_name);
|
|
3611
|
+
const file = await gcsService.uploadFileWithUniqueName(
|
|
3612
|
+
localFilePath,
|
|
3613
|
+
this.config.connector_id,
|
|
3614
|
+
this.streamId
|
|
2919
3615
|
);
|
|
2920
3616
|
const metadata = {
|
|
2921
3617
|
sourceFormat: "NEWLINE_DELIMITED_JSON",
|
|
@@ -2924,6 +3620,17 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
|
|
|
2924
3620
|
await this.loadGCSFileInTable(this.sqlStagingTableId, file, metadata);
|
|
2925
3621
|
} catch (err) {
|
|
2926
3622
|
logger.error(`StreamId: ${this.streamId} - Error while uploading stream.`);
|
|
3623
|
+
if (err.code < 500) {
|
|
3624
|
+
await haltAndCatchFire(
|
|
3625
|
+
`unauthorized`,
|
|
3626
|
+
`We couldn't connect to your Cloud Storage bucket \u{1F614}
|
|
3627
|
+
|
|
3628
|
+
The error from Google is: '${err.message}' \u{1F440}
|
|
3629
|
+
|
|
3630
|
+
Could you troubleshoot your Cloud Storage configuration in Google Cloud and sync again the source? \u{1F64F}`,
|
|
3631
|
+
`Got error from cloud storage lib`
|
|
3632
|
+
);
|
|
3633
|
+
}
|
|
2927
3634
|
throw err;
|
|
2928
3635
|
}
|
|
2929
3636
|
};
|
|
@@ -3026,8 +3733,8 @@ var BigQueryDBSync = class extends StreamWarehouseSyncService {
|
|
|
3026
3733
|
|
|
3027
3734
|
// src/targets/bigquery/main.ts
|
|
3028
3735
|
var BigQueryTarget = class extends ITarget {
|
|
3029
|
-
constructor(config,
|
|
3030
|
-
super(config,
|
|
3736
|
+
constructor(config, stateProvider) {
|
|
3737
|
+
super(config, stateProvider);
|
|
3031
3738
|
this.renameColumnStore.setSafeColumnNameConverter(this.safeColumnNameConverter);
|
|
3032
3739
|
}
|
|
3033
3740
|
static requiredConfigKeys = [
|
|
@@ -3050,15 +3757,61 @@ var BigQueryTarget = class extends ITarget {
|
|
|
3050
3757
|
validateDateRange = validateDateRange;
|
|
3051
3758
|
convertNumberIntoDecimal = convertNumberIntoDecimal;
|
|
3052
3759
|
};
|
|
3760
|
+
|
|
3761
|
+
// src/state-providers/gcs/main.ts
|
|
3762
|
+
import { format as format7 } from "util";
|
|
3763
|
+
var GCSStateProvider = class {
|
|
3764
|
+
bucketName;
|
|
3765
|
+
connectorId;
|
|
3766
|
+
service;
|
|
3767
|
+
constructor(connectorId, bucketName) {
|
|
3768
|
+
this.connectorId = connectorId;
|
|
3769
|
+
this.bucketName = bucketName;
|
|
3770
|
+
this.service = new CloudStorageService(bucketName);
|
|
3771
|
+
}
|
|
3772
|
+
getObjectPath() {
|
|
3773
|
+
return `${this.connectorId}/state.json`;
|
|
3774
|
+
}
|
|
3775
|
+
async getState() {
|
|
3776
|
+
const objectPath = this.getObjectPath();
|
|
3777
|
+
try {
|
|
3778
|
+
logger.info(`\u{1F4C1} Loading state from gs://${this.bucketName}/${objectPath}`);
|
|
3779
|
+
const raw = await this.service.readObjectAsString(objectPath);
|
|
3780
|
+
const parsed = JSON.parse(raw);
|
|
3781
|
+
return { state: parsed };
|
|
3782
|
+
} catch (err) {
|
|
3783
|
+
logger.warn(format7(`error loading state from gs://${this.bucketName}/${objectPath}, will start with empty state, err: %s`, err?.message));
|
|
3784
|
+
return { state: void 0 };
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
async writeState(state) {
|
|
3788
|
+
const objectPath = this.getObjectPath();
|
|
3789
|
+
try {
|
|
3790
|
+
logger.info(`\u{1F4DD} Writing state to gs://${this.bucketName}/${objectPath}`);
|
|
3791
|
+
await this.service.writeStringObject(objectPath, state);
|
|
3792
|
+
} catch (err) {
|
|
3793
|
+
logger.error(format7(`\u{1F4A5} Error while writing state to gs://${this.bucketName}/${objectPath}, err: %s`, err?.message));
|
|
3794
|
+
throw new Error(format7(`error writing state to gs://${this.bucketName}/${objectPath}, err: %s`, err?.message));
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
};
|
|
3053
3798
|
export {
|
|
3054
3799
|
ALL_STREAM_ID_KEYWORD,
|
|
3055
3800
|
API_CALLS_METRIC_NAME,
|
|
3056
3801
|
Authenticator,
|
|
3057
3802
|
BATCH_INTERVAL_MS,
|
|
3058
3803
|
BigQueryTarget,
|
|
3804
|
+
CloudStorageService,
|
|
3059
3805
|
CounterMetric,
|
|
3806
|
+
CsvExtractionConfigBuilder,
|
|
3060
3807
|
DEFAULT_MAX_CONCURRENT_STREAMS,
|
|
3061
3808
|
EXECUTION_TIME_METRIC_NAME,
|
|
3809
|
+
ExcelExtractionConfigBuilder,
|
|
3810
|
+
FileFormat,
|
|
3811
|
+
FilePatterns,
|
|
3812
|
+
FileStream,
|
|
3813
|
+
FileTap,
|
|
3814
|
+
GCSStateProvider,
|
|
3062
3815
|
ITarget,
|
|
3063
3816
|
MissingFieldInSchemaError,
|
|
3064
3817
|
MissingSchemaError,
|
|
@@ -3066,22 +3819,37 @@ export {
|
|
|
3066
3819
|
ROWS_SYNCED_METRIC_NAME,
|
|
3067
3820
|
RecordMessage,
|
|
3068
3821
|
RenameColumnStore,
|
|
3822
|
+
ReplicationMethod,
|
|
3069
3823
|
ReplicationMethodMessage,
|
|
3070
|
-
Resolver,
|
|
3071
3824
|
SchemaMessage,
|
|
3072
3825
|
SchemaValidationError,
|
|
3826
|
+
default2 as SftpClient,
|
|
3827
|
+
SftpService,
|
|
3073
3828
|
StateMessage,
|
|
3074
3829
|
StateService,
|
|
3075
3830
|
Stream,
|
|
3076
|
-
StreamMetadata,
|
|
3077
3831
|
StreamWarehouseSyncService,
|
|
3078
3832
|
Tap,
|
|
3833
|
+
VariableExtractors,
|
|
3079
3834
|
_greaterThan,
|
|
3080
3835
|
addWhalyFields,
|
|
3836
|
+
checkCsvHeaderRow,
|
|
3837
|
+
createCellReference,
|
|
3838
|
+
createCsvStreamConfig,
|
|
3839
|
+
createExcelGenerator,
|
|
3840
|
+
createExcelStreamConfig,
|
|
3081
3841
|
createTemporaryFileStream,
|
|
3842
|
+
csvFieldsToJsonSchema,
|
|
3843
|
+
excelColumnToIndex,
|
|
3844
|
+
excelFieldsToJsonSchema,
|
|
3845
|
+
excelRowToIndex,
|
|
3846
|
+
extractExcelRows,
|
|
3847
|
+
extractPrimaryKeysFromCsvConfig,
|
|
3848
|
+
extractPrimaryKeysFromExcelFields,
|
|
3082
3849
|
extractStateForStream,
|
|
3850
|
+
fieldTypeToJsonSchema,
|
|
3083
3851
|
finalizeStateProgressMarkers,
|
|
3084
|
-
|
|
3852
|
+
findAllMatchingConfigs,
|
|
3085
3853
|
flattenSchema,
|
|
3086
3854
|
getAllMetrics,
|
|
3087
3855
|
getAxiosInstance,
|
|
@@ -3094,16 +3862,23 @@ export {
|
|
|
3094
3862
|
gracefulExit,
|
|
3095
3863
|
haltAndCatchFire,
|
|
3096
3864
|
incrementStreamState,
|
|
3865
|
+
indexToExcelColumn,
|
|
3866
|
+
indexToExcelRow,
|
|
3097
3867
|
loadJson,
|
|
3098
|
-
loadResolver,
|
|
3099
3868
|
loadSchema,
|
|
3100
3869
|
logger,
|
|
3870
|
+
parseCellReference,
|
|
3101
3871
|
postFormDataApiCall,
|
|
3102
3872
|
postJSONApiCall,
|
|
3103
3873
|
postUrlEncodedApiCall,
|
|
3104
3874
|
printMemoryFootprint,
|
|
3875
|
+
processFileStreams,
|
|
3105
3876
|
removeParasiteProperties,
|
|
3877
|
+
rowGeneratorFromCsv,
|
|
3106
3878
|
safePath,
|
|
3107
|
-
|
|
3879
|
+
unzip,
|
|
3880
|
+
validateExcelConfig,
|
|
3881
|
+
writeDataToCsv,
|
|
3882
|
+
writeGeneratorToCSV
|
|
3108
3883
|
};
|
|
3109
3884
|
//# sourceMappingURL=index.mjs.map
|