@whaly/connector-sdk 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +227 -186
- package/dist/index.d.ts +227 -186
- package/dist/index.js +814 -493
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +809 -486
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -2
package/dist/index.mjs
CHANGED
|
@@ -25,10 +25,10 @@ var readAndParseJSONFile = (path) => {
|
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
// src/resolvers/local-file/hook/schemaHook.ts
|
|
28
|
-
import
|
|
28
|
+
import fsExtra from "fs-extra";
|
|
29
29
|
var LocalSchemaHook = class {
|
|
30
30
|
async writeSchema(input) {
|
|
31
|
-
await appendFile("./schema.tmp", JSON.stringify(input) + "\n");
|
|
31
|
+
await fsExtra.appendFile("./schema.tmp", JSON.stringify(input) + "\n");
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
34
|
|
|
@@ -89,15 +89,9 @@ var CounterMetric = class {
|
|
|
89
89
|
|
|
90
90
|
// src/resolvers/local-file/resolver.ts
|
|
91
91
|
var { combine, prettyPrint, timestamp, errors, splat } = winston.format;
|
|
92
|
-
var configPath = "./config.json";
|
|
93
|
-
var targetConfigPath = "./target-config.json";
|
|
94
|
-
var catalogPath = "./catalog.json";
|
|
95
92
|
var statePath = "./state.json";
|
|
96
93
|
var metricsPath = "./metrics.json";
|
|
97
94
|
var LocalFilesResolver = class extends Resolver {
|
|
98
|
-
startVPNIfNeeded() {
|
|
99
|
-
return Promise.resolve();
|
|
100
|
-
}
|
|
101
95
|
checkIfCanSync() {
|
|
102
96
|
return Promise.resolve(true);
|
|
103
97
|
}
|
|
@@ -122,49 +116,15 @@ var LocalFilesResolver = class extends Resolver {
|
|
|
122
116
|
constructor() {
|
|
123
117
|
super();
|
|
124
118
|
}
|
|
125
|
-
|
|
119
|
+
getState() {
|
|
126
120
|
logger.info(`\u{1F4C1} Using the LOCAL resolver`);
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
throw new Error(`\u274C Env variable \`SHORE__DESTINATION\` is not set.
|
|
132
|
-
Did you forget to configure it?
|
|
133
|
-
|
|
134
|
-
Possible values are: \`GOOGLE_BIGQUERY\`, \`SNOWFLAKE\``);
|
|
135
|
-
}
|
|
136
|
-
if (command === `READ`) {
|
|
137
|
-
if (destination === `GOOGLE_BIGQUERY`) {
|
|
138
|
-
const googleCredentials = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
139
|
-
if (!googleCredentials) {
|
|
140
|
-
throw new Error(`Env variable \`GOOGLE_APPLICATION_CREDENTIALS\` is not set.
|
|
141
|
-
Did you forget to configure it?
|
|
142
|
-
|
|
143
|
-
This variable should link to a file containing the proper Google Service Account credentials.
|
|
144
|
-
`);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
const catalog = readAndParseJSONFile(catalogPath);
|
|
148
|
-
const state = readAndParseJSONFile(statePath);
|
|
149
|
-
return Promise.resolve({
|
|
150
|
-
command,
|
|
151
|
-
destination,
|
|
152
|
-
config,
|
|
153
|
-
targetConfig,
|
|
154
|
-
catalog,
|
|
155
|
-
state
|
|
156
|
-
});
|
|
157
|
-
} else {
|
|
158
|
-
return Promise.resolve({
|
|
159
|
-
command,
|
|
160
|
-
destination,
|
|
161
|
-
config,
|
|
162
|
-
targetConfig
|
|
163
|
-
});
|
|
164
|
-
}
|
|
121
|
+
const state = readAndParseJSONFile(statePath);
|
|
122
|
+
return Promise.resolve({
|
|
123
|
+
state
|
|
124
|
+
});
|
|
165
125
|
}
|
|
166
126
|
writeCatalog(catalog) {
|
|
167
|
-
return
|
|
127
|
+
return Promise.resolve();
|
|
168
128
|
}
|
|
169
129
|
writeState(state) {
|
|
170
130
|
return writeFile(statePath, state);
|
|
@@ -195,9 +155,7 @@ var LocalFilesResolver = class extends Resolver {
|
|
|
195
155
|
}
|
|
196
156
|
updateSourceValue(optionKey, optionValue) {
|
|
197
157
|
logger.info(`Updating source value ${optionKey} with value ${optionValue}`, { private: true });
|
|
198
|
-
|
|
199
|
-
config[optionKey] = optionValue;
|
|
200
|
-
return writeFile(configPath, JSON.stringify(config));
|
|
158
|
+
return Promise.resolve();
|
|
201
159
|
}
|
|
202
160
|
getLogFormat() {
|
|
203
161
|
return combine(
|
|
@@ -210,29 +168,8 @@ var LocalFilesResolver = class extends Resolver {
|
|
|
210
168
|
};
|
|
211
169
|
|
|
212
170
|
// src/sdk/service/resolver.ts
|
|
213
|
-
function initResolver(resolverType) {
|
|
214
|
-
if (resolverType === "LOCAL") {
|
|
215
|
-
return new LocalFilesResolver();
|
|
216
|
-
} else {
|
|
217
|
-
throw new Error(`\u274C Unsupported configResolver: ${resolverType}`);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
171
|
var loadResolver = () => {
|
|
221
|
-
|
|
222
|
-
if (!configResolver) {
|
|
223
|
-
throw new Error(`Env variable \`SHORE__CONFIG_RESOLVER\` is not set.
|
|
224
|
-
Did you forget to configure it?
|
|
225
|
-
|
|
226
|
-
Possible values are \`LOCAL\`, \`WHALY\`.
|
|
227
|
-
Please read the documentation to get the full description of the behavior of each.
|
|
228
|
-
`);
|
|
229
|
-
}
|
|
230
|
-
if (configResolver === "WHALY" || configResolver === "LOCAL") {
|
|
231
|
-
return initResolver(configResolver);
|
|
232
|
-
} else {
|
|
233
|
-
throw new Error(`Resolver: ${configResolver} is not supported.
|
|
234
|
-
Did you properly configure \`SHORE__CONFIG_RESOLVER\`?`);
|
|
235
|
-
}
|
|
172
|
+
return new LocalFilesResolver();
|
|
236
173
|
};
|
|
237
174
|
|
|
238
175
|
// src/sdk/service/logger.ts
|
|
@@ -282,30 +219,6 @@ var writeStateFile = (resolver, state) => {
|
|
|
282
219
|
logger.info(`\u{1F4DD} Writing the state file.`);
|
|
283
220
|
return resolver.writeState(state);
|
|
284
221
|
};
|
|
285
|
-
var writeCatalogFile = (resolver, catalog) => {
|
|
286
|
-
logger.info(`\u{1F4DD} Writing the catalog`);
|
|
287
|
-
return resolver.writeCatalog(catalog);
|
|
288
|
-
};
|
|
289
|
-
var loadShoreConfig = (resolver) => {
|
|
290
|
-
const command = process.env.SHORE__COMMAND;
|
|
291
|
-
if (!command) {
|
|
292
|
-
throw new Error(`Env variable \`SHORE__COMMAND\` is not set.
|
|
293
|
-
Did you forget to configure it?
|
|
294
|
-
|
|
295
|
-
Possible values are: \`DISCOVER\`, \`READ\``);
|
|
296
|
-
}
|
|
297
|
-
if (command === `DISCOVER`) {
|
|
298
|
-
logger.info(`\u{1F50E} Mode: discovery
|
|
299
|
-
Shore will detect the available streams in the source and their configuration.`);
|
|
300
|
-
} else if (command === `READ`) {
|
|
301
|
-
logger.info(`\u{1F501} Mode: Synchronizing data
|
|
302
|
-
Shore will extract and push data from the source to the destination.`);
|
|
303
|
-
} else {
|
|
304
|
-
throw new Error(`Command: \`${command}\` is not supported.
|
|
305
|
-
Did you properly configured env variable \`SHORE__COMMAND\`?`);
|
|
306
|
-
}
|
|
307
|
-
return resolver.getConfig(command);
|
|
308
|
-
};
|
|
309
222
|
function readFile(fileName, filePath) {
|
|
310
223
|
try {
|
|
311
224
|
return readFileSync(filePath);
|
|
@@ -331,13 +244,6 @@ var loadSchema = (streamId) => {
|
|
|
331
244
|
const schema = loadJson(`schemas/${streamId}.json`);
|
|
332
245
|
return schema;
|
|
333
246
|
};
|
|
334
|
-
var checkRequiredConfigKeys = (args, requiredConfigKeys) => {
|
|
335
|
-
requiredConfigKeys.forEach((configKey) => {
|
|
336
|
-
if (!args[configKey]) {
|
|
337
|
-
throw new Error(`Config is missing required key: ${configKey}`);
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
};
|
|
341
247
|
|
|
342
248
|
// src/sdk/service/network.ts
|
|
343
249
|
import axios from "axios";
|
|
@@ -615,6 +521,102 @@ var findRelatedRelationships = (streamId, allRels) => {
|
|
|
615
521
|
}, { left: [], right: [] });
|
|
616
522
|
};
|
|
617
523
|
|
|
524
|
+
// src/sdk/models/messages.ts
|
|
525
|
+
var RecordMessage = class {
|
|
526
|
+
type = "RECORD";
|
|
527
|
+
stream;
|
|
528
|
+
record;
|
|
529
|
+
constructor(record, stream) {
|
|
530
|
+
this.record = record;
|
|
531
|
+
this.stream = stream;
|
|
532
|
+
}
|
|
533
|
+
toString() {
|
|
534
|
+
const result = {
|
|
535
|
+
type: "RECORD",
|
|
536
|
+
record: this.record,
|
|
537
|
+
stream: this.stream
|
|
538
|
+
};
|
|
539
|
+
return JSON.stringify(result);
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
var StateMessage = class {
|
|
543
|
+
type = "STATE";
|
|
544
|
+
value;
|
|
545
|
+
constructor(value) {
|
|
546
|
+
this.value = value;
|
|
547
|
+
}
|
|
548
|
+
toString() {
|
|
549
|
+
const result = {
|
|
550
|
+
type: "STATE",
|
|
551
|
+
value: this.value
|
|
552
|
+
};
|
|
553
|
+
return JSON.stringify(result);
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
var ReplicationMethodMessage = class {
|
|
557
|
+
type = "REPLICATION_METHOD";
|
|
558
|
+
stream;
|
|
559
|
+
replication;
|
|
560
|
+
constructor(stream, version) {
|
|
561
|
+
this.stream = stream;
|
|
562
|
+
this.replication = version;
|
|
563
|
+
}
|
|
564
|
+
toString() {
|
|
565
|
+
const result = {
|
|
566
|
+
type: "REPLICATION_METHOD",
|
|
567
|
+
replication: this.replication,
|
|
568
|
+
stream: this.stream
|
|
569
|
+
};
|
|
570
|
+
return JSON.stringify(result);
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
var SchemaMessage = class {
|
|
574
|
+
type = "SCHEMA";
|
|
575
|
+
stream;
|
|
576
|
+
schema;
|
|
577
|
+
keyProperties;
|
|
578
|
+
bookmarkProperties;
|
|
579
|
+
displayLabel;
|
|
580
|
+
description;
|
|
581
|
+
propertiesMetadata;
|
|
582
|
+
relationships;
|
|
583
|
+
constructor(opts) {
|
|
584
|
+
const {
|
|
585
|
+
stream,
|
|
586
|
+
schema,
|
|
587
|
+
keyProperties,
|
|
588
|
+
bookmarkProperties,
|
|
589
|
+
displayLabel,
|
|
590
|
+
description,
|
|
591
|
+
relationships,
|
|
592
|
+
propertiesMetadata
|
|
593
|
+
} = opts;
|
|
594
|
+
this.stream = stream;
|
|
595
|
+
this.schema = schema;
|
|
596
|
+
this.keyProperties = keyProperties;
|
|
597
|
+
this.bookmarkProperties = bookmarkProperties;
|
|
598
|
+
this.displayLabel = displayLabel;
|
|
599
|
+
this.description = description;
|
|
600
|
+
this.relationships = relationships;
|
|
601
|
+
this.propertiesMetadata = propertiesMetadata;
|
|
602
|
+
}
|
|
603
|
+
toString() {
|
|
604
|
+
const result = {
|
|
605
|
+
type: "SCHEMA",
|
|
606
|
+
stream: this.stream,
|
|
607
|
+
schema: this.schema,
|
|
608
|
+
key_properties: this.keyProperties,
|
|
609
|
+
label: this.displayLabel,
|
|
610
|
+
description: this.description,
|
|
611
|
+
relationships: this.relationships
|
|
612
|
+
};
|
|
613
|
+
if (this.bookmarkProperties) {
|
|
614
|
+
result["bookmark_properties"] = this.bookmarkProperties;
|
|
615
|
+
}
|
|
616
|
+
return JSON.stringify(result);
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
|
|
618
620
|
// src/sdk/models/metadata.ts
|
|
619
621
|
var MetadataMap = class extends Map {
|
|
620
622
|
areEquals(array1, array2) {
|
|
@@ -735,155 +737,6 @@ var StreamMetadata = class {
|
|
|
735
737
|
}
|
|
736
738
|
};
|
|
737
739
|
|
|
738
|
-
// src/sdk/models/catalog.ts
|
|
739
|
-
import * as _ from "lodash";
|
|
740
|
-
var Catalog = class _Catalog {
|
|
741
|
-
streams;
|
|
742
|
-
static fromFile(fileName) {
|
|
743
|
-
const catalogFile = loadJson(fileName);
|
|
744
|
-
const catalog = new _Catalog();
|
|
745
|
-
catalog.streams = catalogFile.streams;
|
|
746
|
-
return catalog;
|
|
747
|
-
}
|
|
748
|
-
static fromCatalogFile(catalogFile) {
|
|
749
|
-
const catalog = new _Catalog();
|
|
750
|
-
catalog.streams = catalogFile.streams;
|
|
751
|
-
return catalog;
|
|
752
|
-
}
|
|
753
|
-
getSelectedStreams() {
|
|
754
|
-
if (!this.streams) {
|
|
755
|
-
return [];
|
|
756
|
-
}
|
|
757
|
-
const directSelectedStreams = this.streams.filter((stream) => {
|
|
758
|
-
const metadata = new StreamMetadata();
|
|
759
|
-
metadata.fromCatalogStreamMetadata(stream.metadata);
|
|
760
|
-
return metadata.isStreamSelected();
|
|
761
|
-
});
|
|
762
|
-
logger.debug(`Directly selected streams: ${directSelectedStreams.map((stream) => stream.stream).join(",")}`);
|
|
763
|
-
const getIndirectSelectedStreams = (stream, ancestorsIds) => {
|
|
764
|
-
const currAncestorsIds = ancestorsIds || [];
|
|
765
|
-
const rootBreadcrumbMetadata = stream.metadata.find((metadata) => metadata.breadcrumb.length === 0);
|
|
766
|
-
const parentStreamId = rootBreadcrumbMetadata?.metadata?.["whaly-parent-stream"];
|
|
767
|
-
if (parentStreamId) {
|
|
768
|
-
const parentStream = this.streams?.find((stream2) => stream2.stream === parentStreamId);
|
|
769
|
-
if (parentStream) {
|
|
770
|
-
currAncestorsIds.push(parentStreamId);
|
|
771
|
-
return getIndirectSelectedStreams(parentStream, currAncestorsIds);
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
return currAncestorsIds;
|
|
775
|
-
};
|
|
776
|
-
const indirectSelectedStreamIds = _.uniq(
|
|
777
|
-
directSelectedStreams.flatMap((stream) => {
|
|
778
|
-
return getIndirectSelectedStreams(stream);
|
|
779
|
-
})
|
|
780
|
-
);
|
|
781
|
-
logger.debug(`Indirectly selected streams: ${indirectSelectedStreamIds.join(",")}`);
|
|
782
|
-
const indirectSelectedStreams = indirectSelectedStreamIds.map((streamId) => {
|
|
783
|
-
if (!directSelectedStreams.find((stream) => stream.stream === streamId)) {
|
|
784
|
-
return this.streams?.find((stream) => stream.stream === streamId);
|
|
785
|
-
}
|
|
786
|
-
}).filter((stream) => !!stream);
|
|
787
|
-
return directSelectedStreams.concat(indirectSelectedStreams);
|
|
788
|
-
}
|
|
789
|
-
};
|
|
790
|
-
|
|
791
|
-
// src/sdk/models/messages.ts
|
|
792
|
-
var RecordMessage = class {
|
|
793
|
-
type = "RECORD";
|
|
794
|
-
stream;
|
|
795
|
-
record;
|
|
796
|
-
constructor(record, stream) {
|
|
797
|
-
this.record = record;
|
|
798
|
-
this.stream = stream;
|
|
799
|
-
}
|
|
800
|
-
toString() {
|
|
801
|
-
const result = {
|
|
802
|
-
type: "RECORD",
|
|
803
|
-
record: this.record,
|
|
804
|
-
stream: this.stream
|
|
805
|
-
};
|
|
806
|
-
return JSON.stringify(result);
|
|
807
|
-
}
|
|
808
|
-
};
|
|
809
|
-
var StateMessage = class {
|
|
810
|
-
type = "STATE";
|
|
811
|
-
value;
|
|
812
|
-
constructor(value) {
|
|
813
|
-
this.value = value;
|
|
814
|
-
}
|
|
815
|
-
toString() {
|
|
816
|
-
const result = {
|
|
817
|
-
type: "STATE",
|
|
818
|
-
value: this.value
|
|
819
|
-
};
|
|
820
|
-
return JSON.stringify(result);
|
|
821
|
-
}
|
|
822
|
-
};
|
|
823
|
-
var ReplicationMethodMessage = class {
|
|
824
|
-
type = "REPLICATION_METHOD";
|
|
825
|
-
stream;
|
|
826
|
-
replication;
|
|
827
|
-
constructor(stream, version) {
|
|
828
|
-
this.stream = stream;
|
|
829
|
-
this.replication = version;
|
|
830
|
-
}
|
|
831
|
-
toString() {
|
|
832
|
-
const result = {
|
|
833
|
-
type: "REPLICATION_METHOD",
|
|
834
|
-
replication: this.replication,
|
|
835
|
-
stream: this.stream
|
|
836
|
-
};
|
|
837
|
-
return JSON.stringify(result);
|
|
838
|
-
}
|
|
839
|
-
};
|
|
840
|
-
var SchemaMessage = class {
|
|
841
|
-
type = "SCHEMA";
|
|
842
|
-
stream;
|
|
843
|
-
schema;
|
|
844
|
-
keyProperties;
|
|
845
|
-
bookmarkProperties;
|
|
846
|
-
displayLabel;
|
|
847
|
-
description;
|
|
848
|
-
propertiesMetadata;
|
|
849
|
-
relationships;
|
|
850
|
-
constructor(opts) {
|
|
851
|
-
const {
|
|
852
|
-
stream,
|
|
853
|
-
schema,
|
|
854
|
-
keyProperties,
|
|
855
|
-
bookmarkProperties,
|
|
856
|
-
displayLabel,
|
|
857
|
-
description,
|
|
858
|
-
relationships,
|
|
859
|
-
propertiesMetadata
|
|
860
|
-
} = opts;
|
|
861
|
-
this.stream = stream;
|
|
862
|
-
this.schema = schema;
|
|
863
|
-
this.keyProperties = keyProperties;
|
|
864
|
-
this.bookmarkProperties = bookmarkProperties;
|
|
865
|
-
this.displayLabel = displayLabel;
|
|
866
|
-
this.description = description;
|
|
867
|
-
this.relationships = relationships;
|
|
868
|
-
this.propertiesMetadata = propertiesMetadata;
|
|
869
|
-
}
|
|
870
|
-
toString() {
|
|
871
|
-
const result = {
|
|
872
|
-
type: "SCHEMA",
|
|
873
|
-
stream: this.stream,
|
|
874
|
-
schema: this.schema,
|
|
875
|
-
key_properties: this.keyProperties,
|
|
876
|
-
label: this.displayLabel,
|
|
877
|
-
description: this.description,
|
|
878
|
-
relationships: this.relationships
|
|
879
|
-
};
|
|
880
|
-
if (this.bookmarkProperties) {
|
|
881
|
-
result["bookmark_properties"] = this.bookmarkProperties;
|
|
882
|
-
}
|
|
883
|
-
return JSON.stringify(result);
|
|
884
|
-
}
|
|
885
|
-
};
|
|
886
|
-
|
|
887
740
|
// src/sdk/models/state.ts
|
|
888
741
|
import moment from "moment";
|
|
889
742
|
|
|
@@ -1064,186 +917,46 @@ var isDatetimeType = (typeDef) => {
|
|
|
1064
917
|
};
|
|
1065
918
|
|
|
1066
919
|
// src/sdk/models/tap/stream.ts
|
|
1067
|
-
import
|
|
1068
|
-
import
|
|
920
|
+
import cloneDeep from "lodash/cloneDeep.js";
|
|
921
|
+
import Bluebird2 from "bluebird";
|
|
1069
922
|
|
|
1070
923
|
// src/sdk/models/tap/tap.ts
|
|
1071
|
-
import
|
|
1072
|
-
import { DepGraph } from "dependency-graph";
|
|
924
|
+
import Bluebird from "bluebird";
|
|
1073
925
|
var DEFAULT_MAX_CONCURRENT_STREAMS = 5;
|
|
1074
926
|
var Tap = class {
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
927
|
+
target;
|
|
928
|
+
config;
|
|
929
|
+
streams;
|
|
930
|
+
concurrency = DEFAULT_MAX_CONCURRENT_STREAMS;
|
|
1079
931
|
constructor(target, config) {
|
|
1080
|
-
this.
|
|
1081
|
-
this.
|
|
1082
|
-
this.
|
|
1083
|
-
}
|
|
1084
|
-
discover = async (existingCatalog) => {
|
|
1085
|
-
logger.debug("Loading schemas");
|
|
1086
|
-
const streamDepGraph = new DepGraph();
|
|
1087
|
-
const buildDependencyGraph = (streams2, parentId) => {
|
|
1088
|
-
streams2.forEach((s) => {
|
|
1089
|
-
streamDepGraph.addNode(s.streamId, s);
|
|
1090
|
-
if (parentId) {
|
|
1091
|
-
streamDepGraph.addDependency(s.streamId, parentId);
|
|
1092
|
-
}
|
|
1093
|
-
if (s.children) {
|
|
1094
|
-
return buildDependencyGraph(s.children, s.streamId);
|
|
1095
|
-
}
|
|
1096
|
-
});
|
|
1097
|
-
};
|
|
1098
|
-
buildDependencyGraph(this._streams);
|
|
1099
|
-
const discoveredStreamCatalogEntries = await BPromise.map(
|
|
1100
|
-
streamDepGraph.overallOrder(),
|
|
1101
|
-
async (streamId) => {
|
|
1102
|
-
logger.info(`\u2728 Loading schema for ${streamId}`);
|
|
1103
|
-
const stream = streamDepGraph.getNodeData(streamId);
|
|
1104
|
-
const schema = await stream.getSchema();
|
|
1105
|
-
const parentStreamIds = streamDepGraph.directDependenciesOf(streamId);
|
|
1106
|
-
if (parentStreamIds.length > 1) {
|
|
1107
|
-
throw new Error(`StreamId=${streamId} directly depends on more than 1 stream, e.g. ${parentStreamIds.join(",")} which is not supported!`);
|
|
1108
|
-
}
|
|
1109
|
-
const parentStreamId = parentStreamIds?.[0];
|
|
1110
|
-
const { metadata } = await this.generateMetadataFromStream(stream, parentStreamId);
|
|
1111
|
-
return {
|
|
1112
|
-
schema,
|
|
1113
|
-
metadata,
|
|
1114
|
-
stream: stream.streamId,
|
|
1115
|
-
tap_stream_id: stream.streamId
|
|
1116
|
-
};
|
|
1117
|
-
},
|
|
1118
|
-
{ concurrency: this._concurrency }
|
|
1119
|
-
);
|
|
1120
|
-
const existingCatalogStreamIds = (existingCatalog?.streams || []).map((entry) => {
|
|
1121
|
-
return entry.stream;
|
|
1122
|
-
});
|
|
1123
|
-
const newlyDiscoveredStreamCatalogEntries = discoveredStreamCatalogEntries.filter((entry) => {
|
|
1124
|
-
if (existingCatalogStreamIds.includes(entry.stream)) {
|
|
1125
|
-
return false;
|
|
1126
|
-
} else {
|
|
1127
|
-
return true;
|
|
1128
|
-
}
|
|
1129
|
-
});
|
|
1130
|
-
logger.debug(`Newly discovered streams: %j`, newlyDiscoveredStreamCatalogEntries);
|
|
1131
|
-
const allDiscoveredStreamIds = discoveredStreamCatalogEntries.map((entry) => {
|
|
1132
|
-
return entry.stream;
|
|
1133
|
-
});
|
|
1134
|
-
const existingCatalogMinusDeletedStreams = (existingCatalog?.streams || []).filter((entry) => {
|
|
1135
|
-
if (!allDiscoveredStreamIds.includes(entry.stream)) {
|
|
1136
|
-
logger.debug(`Stream %s was deleted, removing it from the catalog`, entry.stream);
|
|
1137
|
-
return false;
|
|
1138
|
-
} else {
|
|
1139
|
-
return true;
|
|
1140
|
-
}
|
|
1141
|
-
});
|
|
1142
|
-
const updatedExistingCatalog = existingCatalogMinusDeletedStreams.map((existingCatalogEntry) => {
|
|
1143
|
-
const matchingDiscoveredCatalogEntry = discoveredStreamCatalogEntries.find((entry) => entry.stream === existingCatalogEntry.stream);
|
|
1144
|
-
if (matchingDiscoveredCatalogEntry) {
|
|
1145
|
-
const existingRootMetadata = existingCatalogEntry.metadata.find((metadata) => metadata.breadcrumb.length === 0);
|
|
1146
|
-
const discoveredRootMetadata = matchingDiscoveredCatalogEntry.metadata.find((metadata) => metadata.breadcrumb.length === 0);
|
|
1147
|
-
if (existingRootMetadata) {
|
|
1148
|
-
const parentStreamVal = discoveredRootMetadata?.metadata["whaly-parent-stream"];
|
|
1149
|
-
if (parentStreamVal !== void 0) {
|
|
1150
|
-
existingRootMetadata.metadata["whaly-parent-stream"] = parentStreamVal;
|
|
1151
|
-
}
|
|
1152
|
-
const forcedReplicationMethodVal = discoveredRootMetadata?.metadata["forced-replication-method"];
|
|
1153
|
-
if (forcedReplicationMethodVal !== void 0) {
|
|
1154
|
-
existingRootMetadata.metadata["forced-replication-method"] = forcedReplicationMethodVal;
|
|
1155
|
-
}
|
|
1156
|
-
const forcedReplicationKeyVal = discoveredRootMetadata?.metadata["forced-replication-key"];
|
|
1157
|
-
if (forcedReplicationKeyVal !== void 0) {
|
|
1158
|
-
existingRootMetadata.metadata["forced-replication-key"] = forcedReplicationKeyVal;
|
|
1159
|
-
}
|
|
1160
|
-
const validReplicationKeysVal = discoveredRootMetadata?.metadata["valid-replication-keys"];
|
|
1161
|
-
if (validReplicationKeysVal !== void 0) {
|
|
1162
|
-
existingRootMetadata.metadata["valid-replication-keys"] = validReplicationKeysVal;
|
|
1163
|
-
}
|
|
1164
|
-
const rowCountVal = discoveredRootMetadata?.metadata["row-count"];
|
|
1165
|
-
if (rowCountVal !== void 0) {
|
|
1166
|
-
existingRootMetadata.metadata["row-count"] = rowCountVal;
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
return existingCatalogEntry;
|
|
1171
|
-
});
|
|
1172
|
-
const streams = updatedExistingCatalog.concat(newlyDiscoveredStreamCatalogEntries);
|
|
1173
|
-
return { streams };
|
|
1174
|
-
};
|
|
1175
|
-
sync = async (catalog) => {
|
|
1176
|
-
await this.initiateSync(catalog);
|
|
1177
|
-
logger.debug(`[TAP] All streams have finished syncing. We send the [complete] signal to the target.`);
|
|
1178
|
-
await this._target.complete();
|
|
1179
|
-
return Promise.resolve();
|
|
1180
|
-
};
|
|
1181
|
-
async generateMetadataFromStream(stream, parentStreamId) {
|
|
1182
|
-
const metadata = new StreamMetadata();
|
|
1183
|
-
metadata.write([], "table-key-properties", stream.primaryKey);
|
|
1184
|
-
const defaultReplicationConfig = stream.defaultReplicationConfig();
|
|
1185
|
-
metadata.write([], "replication-method", defaultReplicationConfig.replicationMethod);
|
|
1186
|
-
const validReplicationkeys = await stream.getValidReplicationKeys();
|
|
1187
|
-
metadata.write([], "valid-replication-keys", validReplicationkeys);
|
|
1188
|
-
if (defaultReplicationConfig.isForced) {
|
|
1189
|
-
metadata.write([], "forced-replication-method", defaultReplicationConfig.replicationMethod);
|
|
1190
|
-
if (stream.forcedReplicationKey) {
|
|
1191
|
-
metadata.write([], "forced-replication-key", stream.forcedReplicationKey);
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
const rowCount = await stream.getRowCount();
|
|
1195
|
-
if (rowCount !== void 0) {
|
|
1196
|
-
metadata.write([], "row-count", rowCount);
|
|
1197
|
-
}
|
|
1198
|
-
if (stream.isSilent === true) {
|
|
1199
|
-
metadata.write([], "whaly-is-hidden", stream.isSilent);
|
|
1200
|
-
}
|
|
1201
|
-
if (stream.isSilent === false) {
|
|
1202
|
-
metadata.write([], "selected-by-default", stream.selectedByDefault);
|
|
1203
|
-
}
|
|
1204
|
-
if (stream.displayLabel) {
|
|
1205
|
-
metadata.write([], "whaly-display-label", stream.displayLabel);
|
|
1206
|
-
}
|
|
1207
|
-
if (stream.description) {
|
|
1208
|
-
metadata.write([], "whaly-description", stream.description);
|
|
1209
|
-
}
|
|
1210
|
-
if (parentStreamId !== void 0) {
|
|
1211
|
-
metadata.write([], "whaly-parent-stream", parentStreamId);
|
|
1212
|
-
}
|
|
1213
|
-
return { metadata: metadata.toList() };
|
|
932
|
+
this.streams = [];
|
|
933
|
+
this.target = target;
|
|
934
|
+
this.config = config;
|
|
1214
935
|
}
|
|
1215
|
-
async
|
|
936
|
+
sync = async (options) => {
|
|
1216
937
|
logger.info(`\u{1F680} Start syncing`);
|
|
1217
938
|
const state = StateService.getInstance().get();
|
|
1218
939
|
logger.info(`\u{1F4CD} Received state: %j`, state);
|
|
1219
|
-
|
|
1220
|
-
const
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
return stream.sync();
|
|
1240
|
-
}
|
|
1241
|
-
},
|
|
1242
|
-
{
|
|
1243
|
-
concurrency: this._concurrency
|
|
1244
|
-
}
|
|
1245
|
-
);
|
|
1246
|
-
}
|
|
940
|
+
const envInclude = process.env.TAP_STREAMS ? process.env.TAP_STREAMS.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : void 0;
|
|
941
|
+
const include = options?.include && options.include.length > 0 ? options.include : envInclude;
|
|
942
|
+
const exclude = new Set((options?.exclude || []).map((s) => s.trim()));
|
|
943
|
+
let streamsToRun = this.streams.filter((s) => !s.isSilent);
|
|
944
|
+
if (include && include.length > 0) {
|
|
945
|
+
const includeSet = new Set(include);
|
|
946
|
+
streamsToRun = streamsToRun.filter((s) => includeSet.has(s.streamId));
|
|
947
|
+
}
|
|
948
|
+
if (exclude.size > 0) {
|
|
949
|
+
streamsToRun = streamsToRun.filter((s) => !exclude.has(s.streamId));
|
|
950
|
+
}
|
|
951
|
+
const selectedStreamIds = streamsToRun.map((s) => s.streamId);
|
|
952
|
+
logger.info(`\u{1F973} Syncing streams: %s`, selectedStreamIds.join(","));
|
|
953
|
+
await Bluebird.map(streamsToRun, (stream) => {
|
|
954
|
+
return stream.sync();
|
|
955
|
+
}, { concurrency: this.concurrency });
|
|
956
|
+
logger.debug(`[TAP] All streams have finished syncing. We send the [complete] signal to the target.`);
|
|
957
|
+
await this.target.complete();
|
|
958
|
+
return Promise.resolve();
|
|
959
|
+
};
|
|
1247
960
|
// Used to clean up any ressource before exit
|
|
1248
961
|
end() {
|
|
1249
962
|
return Promise.resolve();
|
|
@@ -1252,15 +965,11 @@ var Tap = class {
|
|
|
1252
965
|
|
|
1253
966
|
// src/sdk/models/tap/stream.ts
|
|
1254
967
|
import { format as format2 } from "util";
|
|
1255
|
-
var
|
|
968
|
+
var Stream = class {
|
|
1256
969
|
// Runtime values, can't be overriden
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
_target;
|
|
1261
|
-
// Contains the root metadata entry of the catalog stream configuration
|
|
1262
|
-
// contains some end user schema configuration
|
|
1263
|
-
rootMetadataEntry;
|
|
970
|
+
config;
|
|
971
|
+
tapState;
|
|
972
|
+
target;
|
|
1264
973
|
// Compile time values, can be overriden
|
|
1265
974
|
// Replication method that can be forced in the stream implemenation. Will prevail over any user/catalog choice
|
|
1266
975
|
forcedReplicationMethod = void 0;
|
|
@@ -1286,11 +995,10 @@ var StreamV2 = class {
|
|
|
1286
995
|
// Metrics configuration
|
|
1287
996
|
rowsSyncedMetricsConf;
|
|
1288
997
|
executionTimeMetricsConf;
|
|
1289
|
-
constructor(
|
|
1290
|
-
this.
|
|
1291
|
-
this.
|
|
1292
|
-
this.
|
|
1293
|
-
this._target = target;
|
|
998
|
+
constructor(config, state, target, childConcurrency = DEFAULT_MAX_CONCURRENT_STREAMS) {
|
|
999
|
+
this.config = config;
|
|
1000
|
+
this.tapState = cloneDeep(state);
|
|
1001
|
+
this.target = target;
|
|
1294
1002
|
this.childConcurrency = childConcurrency;
|
|
1295
1003
|
this.rowsSyncedMetricsConf = {
|
|
1296
1004
|
isEnabled: () => !this.isSilent,
|
|
@@ -1301,9 +1009,7 @@ var StreamV2 = class {
|
|
|
1301
1009
|
getStreamIds: () => [this.streamId]
|
|
1302
1010
|
};
|
|
1303
1011
|
}
|
|
1304
|
-
|
|
1305
|
-
this.rootMetadataEntry = rootMetadataEntry;
|
|
1306
|
-
};
|
|
1012
|
+
// No root metadata: replication config is derived from stream defaults only
|
|
1307
1013
|
/**
|
|
1308
1014
|
* Sync this stream
|
|
1309
1015
|
* Called only for streams with no parents, aka. "root tap streams"
|
|
@@ -1334,7 +1040,7 @@ var StreamV2 = class {
|
|
|
1334
1040
|
`\u{1F6BD} Flushing ${replicationMethod} sync of stream '${this.streamId}' + all of its children...`
|
|
1335
1041
|
);
|
|
1336
1042
|
await this._flush();
|
|
1337
|
-
await
|
|
1043
|
+
await Bluebird2.map(this.children, (child) => {
|
|
1338
1044
|
return child.flush();
|
|
1339
1045
|
}, { concurrency: 3 });
|
|
1340
1046
|
};
|
|
@@ -1344,7 +1050,7 @@ var StreamV2 = class {
|
|
|
1344
1050
|
*/
|
|
1345
1051
|
_writeStateMessage = () => {
|
|
1346
1052
|
const message = new StateMessage(StateService.getInstance().get());
|
|
1347
|
-
return this.
|
|
1053
|
+
return this.target.state(message);
|
|
1348
1054
|
};
|
|
1349
1055
|
/**
|
|
1350
1056
|
* Write out a ACTIVATE_VERSION message.
|
|
@@ -1352,10 +1058,10 @@ var StreamV2 = class {
|
|
|
1352
1058
|
_writeReplicationMethodMessage = async () => {
|
|
1353
1059
|
const method = this.configuredReplicationConfig().replicationMethod;
|
|
1354
1060
|
const message = new ReplicationMethodMessage(this.streamId, method);
|
|
1355
|
-
await
|
|
1061
|
+
await Bluebird2.map(this.children, async (c) => {
|
|
1356
1062
|
return c._writeReplicationMethodMessage();
|
|
1357
1063
|
}, { concurrency: 3 });
|
|
1358
|
-
await this.
|
|
1064
|
+
await this.target.replicationMethod(message);
|
|
1359
1065
|
};
|
|
1360
1066
|
/**
|
|
1361
1067
|
* Write out a SCHEMA message with the stream schema.
|
|
@@ -1376,11 +1082,11 @@ var StreamV2 = class {
|
|
|
1376
1082
|
...this.description ? { description: this.description } : {},
|
|
1377
1083
|
...schema.propertiesMetadata ? { propertiesMetadata: schema.propertiesMetadata } : {}
|
|
1378
1084
|
});
|
|
1379
|
-
await this.
|
|
1085
|
+
await this.target.schema(message);
|
|
1380
1086
|
}
|
|
1381
1087
|
}
|
|
1382
1088
|
if (skipChildrenSchema !== true) {
|
|
1383
|
-
await
|
|
1089
|
+
await Bluebird2.map(this.children, async (child) => {
|
|
1384
1090
|
await child._writeSchemaMessage();
|
|
1385
1091
|
}, { concurrency: 3 });
|
|
1386
1092
|
}
|
|
@@ -1455,8 +1161,8 @@ var StreamV2 = class {
|
|
|
1455
1161
|
**/
|
|
1456
1162
|
getStartingTimestamp() {
|
|
1457
1163
|
const streamIdToReadFrom = this.useStateFromStreamId ? this.useStateFromStreamId : this.streamId;
|
|
1458
|
-
const state = extractStateForStream(this.
|
|
1459
|
-
const startDate = this.
|
|
1164
|
+
const state = extractStateForStream(this.tapState, streamIdToReadFrom);
|
|
1165
|
+
const startDate = this.config.start_date;
|
|
1460
1166
|
const replicationConfig = this.configuredReplicationConfig();
|
|
1461
1167
|
logger.debug(`${this.streamId}: state.replicationKeyValue: ${state.replicationKeyValue},
|
|
1462
1168
|
state.replicationKey: ${state.replicationKey},
|
|
@@ -1515,16 +1221,10 @@ var StreamV2 = class {
|
|
|
1515
1221
|
* Return the configured replication method that will be used for the sync.
|
|
1516
1222
|
*/
|
|
1517
1223
|
configuredReplicationConfig = () => {
|
|
1518
|
-
const
|
|
1519
|
-
if (defaultReplicationMethod.isForced) {
|
|
1520
|
-
return {
|
|
1521
|
-
replicationMethod: defaultReplicationMethod.replicationMethod,
|
|
1522
|
-
replicationKey: defaultReplicationMethod.replicationKey
|
|
1523
|
-
};
|
|
1524
|
-
}
|
|
1224
|
+
const defaults = this.defaultReplicationConfig();
|
|
1525
1225
|
return {
|
|
1526
|
-
replicationMethod:
|
|
1527
|
-
replicationKey:
|
|
1226
|
+
replicationMethod: defaults.replicationMethod,
|
|
1227
|
+
replicationKey: defaults.replicationKey
|
|
1528
1228
|
};
|
|
1529
1229
|
};
|
|
1530
1230
|
// Private sync methods:
|
|
@@ -1554,9 +1254,9 @@ var StreamV2 = class {
|
|
|
1554
1254
|
this.streamId
|
|
1555
1255
|
);
|
|
1556
1256
|
if (!this.isSilent) {
|
|
1557
|
-
await this.
|
|
1257
|
+
await this.target.record(recordMessage);
|
|
1558
1258
|
}
|
|
1559
|
-
await
|
|
1259
|
+
await Bluebird2.map(
|
|
1560
1260
|
this.children,
|
|
1561
1261
|
async (child) => {
|
|
1562
1262
|
await child.asyncInit(row);
|
|
@@ -1600,7 +1300,7 @@ var StreamV2 = class {
|
|
|
1600
1300
|
getSchema() {
|
|
1601
1301
|
logger.debug(`stream: ${this.streamId} - getSchema() is called`);
|
|
1602
1302
|
if (this.schemaPath) {
|
|
1603
|
-
const schema = loadJson(`schemas/${this.
|
|
1303
|
+
const schema = loadJson(`schemas/${this.schemaPath}`);
|
|
1604
1304
|
return Promise.resolve({ jsonSchema: schema });
|
|
1605
1305
|
} else {
|
|
1606
1306
|
if (this.isSilent) {
|
|
@@ -1637,7 +1337,7 @@ var StreamV2 = class {
|
|
|
1637
1337
|
};
|
|
1638
1338
|
|
|
1639
1339
|
// src/sdk/helpers/qs-logger.ts
|
|
1640
|
-
import
|
|
1340
|
+
import isPlainObject from "lodash/isPlainObject.js";
|
|
1641
1341
|
var MAX_QS_KEYS = 10;
|
|
1642
1342
|
var MAX_QS_STRING_LENGTH = 200;
|
|
1643
1343
|
var MAX_QS_ARRAY_ITEMS = 5;
|
|
@@ -1658,7 +1358,7 @@ function truncateQueryParams(value, depth = 0) {
|
|
|
1658
1358
|
}
|
|
1659
1359
|
return items;
|
|
1660
1360
|
}
|
|
1661
|
-
if (
|
|
1361
|
+
if (isPlainObject(value)) {
|
|
1662
1362
|
const keys = Object.keys(value);
|
|
1663
1363
|
const result = {};
|
|
1664
1364
|
for (const key of keys.slice(0, MAX_QS_KEYS)) {
|
|
@@ -1681,11 +1381,12 @@ function getTruncatedParamsForLog(params) {
|
|
|
1681
1381
|
}
|
|
1682
1382
|
|
|
1683
1383
|
// src/sdk/models/tap/restStream.ts
|
|
1684
|
-
import
|
|
1685
|
-
|
|
1384
|
+
import cloneDeep2 from "lodash/cloneDeep.js";
|
|
1385
|
+
import isEqual from "lodash/isEqual.js";
|
|
1386
|
+
var RESTStream = class extends Stream {
|
|
1686
1387
|
axiosInstance;
|
|
1687
|
-
constructor(
|
|
1688
|
-
super(
|
|
1388
|
+
constructor(config, state, target, childConcurrency = DEFAULT_MAX_CONCURRENT_STREAMS) {
|
|
1389
|
+
super(config, state, target, childConcurrency);
|
|
1689
1390
|
this.apiCallsMetricsConf = {
|
|
1690
1391
|
isEnabled: () => !this.isSilent,
|
|
1691
1392
|
getStreamIds: () => [this.streamId]
|
|
@@ -1708,7 +1409,7 @@ var RESTStreamV2 = class extends StreamV2 {
|
|
|
1708
1409
|
const authenticator = this.authenticator;
|
|
1709
1410
|
const params = {
|
|
1710
1411
|
...this.getNextUrlParams(nextPageToken, parent),
|
|
1711
|
-
...await authenticator?.getAuthQS(this.
|
|
1412
|
+
...await authenticator?.getAuthQS(this.config)
|
|
1712
1413
|
};
|
|
1713
1414
|
const requestBody = this.getRequestBodyForNextCall(nextPageToken, parent);
|
|
1714
1415
|
let headers = this.getCustomHTTPHeaders(parent);
|
|
@@ -1716,7 +1417,7 @@ var RESTStreamV2 = class extends StreamV2 {
|
|
|
1716
1417
|
headers = {};
|
|
1717
1418
|
}
|
|
1718
1419
|
if (authenticator) {
|
|
1719
|
-
const authHeaders = await authenticator.getAuthHeaders(this.
|
|
1420
|
+
const authHeaders = await authenticator.getAuthHeaders(this.config);
|
|
1720
1421
|
headers = {
|
|
1721
1422
|
...headers,
|
|
1722
1423
|
...authHeaders
|
|
@@ -1799,9 +1500,9 @@ var RESTStreamV2 = class extends StreamV2 {
|
|
|
1799
1500
|
for await (const row of rows) {
|
|
1800
1501
|
yield row;
|
|
1801
1502
|
}
|
|
1802
|
-
const previousToken =
|
|
1503
|
+
const previousToken = cloneDeep2(nextPageToken);
|
|
1803
1504
|
nextPageToken = this.getNextPageToken(resp, previousToken);
|
|
1804
|
-
if (nextPageToken &&
|
|
1505
|
+
if (nextPageToken && isEqual(nextPageToken, previousToken)) {
|
|
1805
1506
|
throw new Error(
|
|
1806
1507
|
`${this.streamId} - Loop detected in pagination. "
|
|
1807
1508
|
Pagination token: \`${JSON.stringify(nextPageToken)}\` is identical to prior token \`${JSON.stringify(previousToken)}\`.`
|
|
@@ -2002,20 +1703,20 @@ var StreamWarehouseSyncService = class {
|
|
|
2002
1703
|
const columnNamesFromWarehouse = columnsFromWarehouse.map((col) => col.name);
|
|
2003
1704
|
const columnsMappingStore = this.renamedColumnStore.getUnsafeToSafeColumnMapping(this.streamId);
|
|
2004
1705
|
const columnsToAdd = Object.keys(columnsMappingStore).filter((unsafeColumnKey) => {
|
|
2005
|
-
const
|
|
2006
|
-
if (!
|
|
1706
|
+
const safeColumnName2 = columnsMappingStore[unsafeColumnKey];
|
|
1707
|
+
if (!safeColumnName2) {
|
|
2007
1708
|
return false;
|
|
2008
1709
|
}
|
|
2009
|
-
return !columnNamesFromWarehouse.includes(
|
|
1710
|
+
return !columnNamesFromWarehouse.includes(safeColumnName2);
|
|
2010
1711
|
}).map((unsafeColumnKey) => {
|
|
2011
|
-
const
|
|
1712
|
+
const safeColumnName2 = columnsMappingStore[unsafeColumnKey];
|
|
2012
1713
|
const definition = this.flattenedSchema[unsafeColumnKey];
|
|
2013
|
-
if (!
|
|
1714
|
+
if (!safeColumnName2 || !definition) {
|
|
2014
1715
|
return void 0;
|
|
2015
1716
|
}
|
|
2016
1717
|
return {
|
|
2017
1718
|
unsafeName: unsafeColumnKey,
|
|
2018
|
-
name:
|
|
1719
|
+
name: safeColumnName2,
|
|
2019
1720
|
definition
|
|
2020
1721
|
};
|
|
2021
1722
|
}).filter((item) => !!item);
|
|
@@ -2237,7 +1938,7 @@ import moment3 from "moment";
|
|
|
2237
1938
|
|
|
2238
1939
|
// src/sdk/models/target/temporaryFile.ts
|
|
2239
1940
|
import fs3 from "fs-extra";
|
|
2240
|
-
import
|
|
1941
|
+
import uniqueId from "lodash/uniqueId.js";
|
|
2241
1942
|
var createTemporaryFileStream = (streamId) => {
|
|
2242
1943
|
fs3.ensureDirSync("./tmp");
|
|
2243
1944
|
const path = `./tmp/${safePath(streamId)}-${uniqueId()}.tmp`;
|
|
@@ -2365,7 +2066,7 @@ var StreamState = class {
|
|
|
2365
2066
|
|
|
2366
2067
|
// src/sdk/models/target/target.ts
|
|
2367
2068
|
import { Semaphore } from "async-mutex";
|
|
2368
|
-
import
|
|
2069
|
+
import Bluebird3 from "bluebird";
|
|
2369
2070
|
var semaphore = new Semaphore(1);
|
|
2370
2071
|
var ITarget = class _ITarget {
|
|
2371
2072
|
config;
|
|
@@ -2610,7 +2311,7 @@ var ITarget = class _ITarget {
|
|
|
2610
2311
|
})
|
|
2611
2312
|
}
|
|
2612
2313
|
};
|
|
2613
|
-
await
|
|
2314
|
+
await Bluebird3.map(this.schemaHooks, async (hook) => {
|
|
2614
2315
|
const input = {
|
|
2615
2316
|
databaseName,
|
|
2616
2317
|
schemaName,
|
|
@@ -2691,7 +2392,7 @@ var ITarget = class _ITarget {
|
|
|
2691
2392
|
};
|
|
2692
2393
|
uploadStreamsToWarehouse = async (streamsToUpload) => {
|
|
2693
2394
|
try {
|
|
2694
|
-
await
|
|
2395
|
+
await Bluebird3.map(streamsToUpload, async (streamId) => {
|
|
2695
2396
|
logger.info(`\u{1F4E4} Upload stream: ${streamId} to Warehouse`);
|
|
2696
2397
|
const streamState = this.streams[streamId];
|
|
2697
2398
|
if (streamState) {
|
|
@@ -2725,19 +2426,643 @@ var ITarget = class _ITarget {
|
|
|
2725
2426
|
}
|
|
2726
2427
|
};
|
|
2727
2428
|
};
|
|
2429
|
+
|
|
2430
|
+
// src/targets/bigquery/helpers.ts
|
|
2431
|
+
var safeColumnName = (key) => {
|
|
2432
|
+
let returnString = key;
|
|
2433
|
+
const shouldNotStartWith = ["_TABLE_", "_FILE_", "_PARTITION"];
|
|
2434
|
+
shouldNotStartWith.forEach((snm) => {
|
|
2435
|
+
if (returnString.startsWith(snm)) {
|
|
2436
|
+
returnString = returnString.replace(snm, "");
|
|
2437
|
+
}
|
|
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
|
+
}
|
|
2446
|
+
const pattern = /[^a-zA-Z0-9_]/gm;
|
|
2447
|
+
return returnString.replace(pattern, "_");
|
|
2448
|
+
};
|
|
2449
|
+
var safeTableName = (key) => {
|
|
2450
|
+
let returnString = key;
|
|
2451
|
+
returnString = returnString.toLowerCase();
|
|
2452
|
+
returnString = returnString.replace("`", "");
|
|
2453
|
+
returnString = returnString.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
2454
|
+
if (returnString.length > 1024) {
|
|
2455
|
+
returnString = returnString.substr(0, 1024);
|
|
2456
|
+
}
|
|
2457
|
+
const pattern = /[^a-zA-Z0-9_]/gm;
|
|
2458
|
+
return returnString.replace(pattern, "_");
|
|
2459
|
+
};
|
|
2460
|
+
|
|
2461
|
+
// src/targets/bigquery/service/record.ts
|
|
2462
|
+
import Decimal from "decimal.js";
|
|
2463
|
+
import moment5 from "moment";
|
|
2464
|
+
var validateDateRange = (record, schema) => {
|
|
2465
|
+
Object.keys(schema).forEach((key) => {
|
|
2466
|
+
const { format: format3 } = schema[key];
|
|
2467
|
+
if (format3 === "date-time") {
|
|
2468
|
+
if (record[key]) {
|
|
2469
|
+
const formattedDate = moment5(record[key]).format(defaultDateTimeFormat);
|
|
2470
|
+
const isNotTooMuchInTheFuture = moment5(record[key]).isBefore(moment5("9999-12-31 23:59:59.999999"));
|
|
2471
|
+
const isNotTooMuchInThePast = moment5(record[key]).isAfter(moment5("0001-01-01 00:00:00"));
|
|
2472
|
+
if (formattedDate !== "Invalid date" && isNotTooMuchInTheFuture && isNotTooMuchInThePast) {
|
|
2473
|
+
record[key] = formattedDate;
|
|
2474
|
+
} else {
|
|
2475
|
+
record[key] = void 0;
|
|
2476
|
+
logger.info("Skipping date for key %s for being invalid", key);
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
});
|
|
2481
|
+
return record;
|
|
2482
|
+
};
|
|
2483
|
+
var maxBQNumericValue = new Decimal("9.9999999999999999999999999999999999999E+28");
|
|
2484
|
+
var convertNumberIntoDecimal = (record, schema) => {
|
|
2485
|
+
Object.keys(record).forEach((key) => {
|
|
2486
|
+
if (schema[key]) {
|
|
2487
|
+
const { type, items } = schema[key];
|
|
2488
|
+
const types = type instanceof Array ? type : [type];
|
|
2489
|
+
if (types.includes("number")) {
|
|
2490
|
+
if (typeof record[key] === "number" || record[key] instanceof Decimal) {
|
|
2491
|
+
const decimal = new Decimal(record[key]).toDecimalPlaces(9);
|
|
2492
|
+
if (decimal.greaterThanOrEqualTo(maxBQNumericValue)) {
|
|
2493
|
+
record[key] = null;
|
|
2494
|
+
} else {
|
|
2495
|
+
record[key] = decimal;
|
|
2496
|
+
}
|
|
2497
|
+
} else {
|
|
2498
|
+
record[key] = null;
|
|
2499
|
+
}
|
|
2500
|
+
} else if (types.includes("array") && items?.type.includes("number")) {
|
|
2501
|
+
if (Array.isArray(record[key])) {
|
|
2502
|
+
const rawArr = record[key];
|
|
2503
|
+
const newArr = rawArr.map((item) => {
|
|
2504
|
+
if (typeof item === "number" || item instanceof Decimal) {
|
|
2505
|
+
const parsed = new Decimal(item).toDecimalPlaces(9);
|
|
2506
|
+
if (parsed.greaterThanOrEqualTo(maxBQNumericValue)) {
|
|
2507
|
+
return void 0;
|
|
2508
|
+
} else {
|
|
2509
|
+
return parsed;
|
|
2510
|
+
}
|
|
2511
|
+
} else {
|
|
2512
|
+
return void 0;
|
|
2513
|
+
}
|
|
2514
|
+
}).filter((item) => !!item);
|
|
2515
|
+
record[key] = newArr;
|
|
2516
|
+
} else {
|
|
2517
|
+
record[key] = null;
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
});
|
|
2522
|
+
return record;
|
|
2523
|
+
};
|
|
2524
|
+
|
|
2525
|
+
// src/targets/bigquery/service/dbSync.ts
|
|
2526
|
+
import moment6 from "moment";
|
|
2527
|
+
|
|
2528
|
+
// src/targets/bigquery/service/bigquery.ts
|
|
2529
|
+
import { BigQuery } from "@google-cloud/bigquery";
|
|
2530
|
+
var BqClientHolder = class {
|
|
2531
|
+
static instance;
|
|
2532
|
+
client;
|
|
2533
|
+
constructor(config) {
|
|
2534
|
+
const retryOptions = {
|
|
2535
|
+
autoRetry: true,
|
|
2536
|
+
maxRetries: 3
|
|
2537
|
+
};
|
|
2538
|
+
this.client = new BigQuery({
|
|
2539
|
+
projectId: config.database.trim(),
|
|
2540
|
+
...retryOptions
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
static client(config) {
|
|
2544
|
+
if (!this.instance) {
|
|
2545
|
+
this.instance = new this(config);
|
|
2546
|
+
}
|
|
2547
|
+
return this.instance.client;
|
|
2548
|
+
}
|
|
2549
|
+
};
|
|
2550
|
+
|
|
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
|
+
// src/targets/bigquery/service/dbSync.ts
|
|
2642
|
+
import chunk from "lodash/chunk.js";
|
|
2643
|
+
import { Semaphore as Semaphore2 } from "async-mutex";
|
|
2644
|
+
var semaphore2 = new Semaphore2(1);
|
|
2645
|
+
var BigQueryDBSync = class extends StreamWarehouseSyncService {
|
|
2646
|
+
config;
|
|
2647
|
+
bqClient;
|
|
2648
|
+
bqDataset;
|
|
2649
|
+
bqTable;
|
|
2650
|
+
bqStagingTable;
|
|
2651
|
+
tableName;
|
|
2652
|
+
stagingTableName;
|
|
2653
|
+
sqlTableId;
|
|
2654
|
+
sqlStagingTableId;
|
|
2655
|
+
constructor(config, streamId, database, schema, table, renameColumnStore) {
|
|
2656
|
+
super(
|
|
2657
|
+
streamId,
|
|
2658
|
+
database,
|
|
2659
|
+
schema,
|
|
2660
|
+
table,
|
|
2661
|
+
renameColumnStore
|
|
2662
|
+
);
|
|
2663
|
+
this.config = config;
|
|
2664
|
+
this.bqClient = BqClientHolder.client(config);
|
|
2665
|
+
this.bqDataset = this.bqClient.dataset(schema, { location: config.location || "EU" });
|
|
2666
|
+
this.bqTable = this.bqDataset.table(table, { location: config.location || "EU" });
|
|
2667
|
+
this.tableName = table;
|
|
2668
|
+
this.stagingTableName = `${table}_temp`;
|
|
2669
|
+
this.bqStagingTable = this.bqDataset.table(this.stagingTableName, { location: config.location || "EU" });
|
|
2670
|
+
this.sqlTableId = `${schema}.${table}`;
|
|
2671
|
+
this.sqlStagingTableId = `${this.sqlTableId}_temp`;
|
|
2672
|
+
}
|
|
2673
|
+
// Implementation of abstract classes
|
|
2674
|
+
getSerializedRecord = (record) => {
|
|
2675
|
+
return JSON.stringify(record) + `
|
|
2676
|
+
`;
|
|
2677
|
+
};
|
|
2678
|
+
safeColumnName = safeColumnName;
|
|
2679
|
+
getWarehouseTypeFromJSONSchema = (definition) => {
|
|
2680
|
+
const type = definition.type;
|
|
2681
|
+
const format3 = definition.format;
|
|
2682
|
+
let typeArr;
|
|
2683
|
+
if (type instanceof Array) {
|
|
2684
|
+
typeArr = type;
|
|
2685
|
+
} else {
|
|
2686
|
+
typeArr = [type];
|
|
2687
|
+
}
|
|
2688
|
+
if (format3) {
|
|
2689
|
+
switch (format3) {
|
|
2690
|
+
case "date-time":
|
|
2691
|
+
return "TIMESTAMP";
|
|
2692
|
+
case "json":
|
|
2693
|
+
return "STRING";
|
|
2694
|
+
default:
|
|
2695
|
+
throw new Error(`StreamId: ${this.streamId} - Unsupported format: ${format3}`);
|
|
2696
|
+
}
|
|
2697
|
+
} else if (typeArr.includes("number")) {
|
|
2698
|
+
return "NUMERIC";
|
|
2699
|
+
} else if (typeArr.includes("integer") && type.includes("string")) {
|
|
2700
|
+
return "NUMERIC";
|
|
2701
|
+
} else if (typeArr.includes("integer")) {
|
|
2702
|
+
return "INTEGER";
|
|
2703
|
+
} else if (typeArr.includes("boolean")) {
|
|
2704
|
+
return "BOOLEAN";
|
|
2705
|
+
} else if (typeArr.includes("string")) {
|
|
2706
|
+
return "STRING";
|
|
2707
|
+
} else if (typeArr.includes("array")) {
|
|
2708
|
+
const items = definition.items;
|
|
2709
|
+
if (items) {
|
|
2710
|
+
return this.getWarehouseTypeFromJSONSchema(items);
|
|
2711
|
+
}
|
|
2712
|
+
throw new Error(`StreamId: ${this.streamId} - Unsupported array schema field definition: ${JSON.stringify(definition)}`);
|
|
2713
|
+
} else {
|
|
2714
|
+
throw new Error(`StreamId: ${this.streamId} - Unsupported schema field definition: ${JSON.stringify(definition)}`);
|
|
2715
|
+
}
|
|
2716
|
+
};
|
|
2717
|
+
getMergeQueries = () => {
|
|
2718
|
+
const primaryKeyCondition = this.primaryKeys.map((pk) => {
|
|
2719
|
+
return `\`sourced\`.\`${pk}\` = \`destination\`.\`${pk}\``;
|
|
2720
|
+
}).join(` AND `);
|
|
2721
|
+
const primaryKeys = this.primaryKeys.map((pk) => `\`${pk}\``).join(` ,`);
|
|
2722
|
+
const columnUnsafeToSafeMapping = this.renamedColumnStore.getUnsafeToSafeColumnMapping(this.streamId);
|
|
2723
|
+
const columns = Object.keys(columnUnsafeToSafeMapping).map((column) => {
|
|
2724
|
+
return {
|
|
2725
|
+
isUnsafe: true,
|
|
2726
|
+
colName: column
|
|
2727
|
+
};
|
|
2728
|
+
});
|
|
2729
|
+
const columnsChunks = chunk(columns, 300).map((chunk2) => {
|
|
2730
|
+
this.primaryKeys.forEach((pk) => {
|
|
2731
|
+
if (!chunk2.map((c) => columnUnsafeToSafeMapping[c.colName]).includes(pk)) {
|
|
2732
|
+
chunk2.push({
|
|
2733
|
+
isUnsafe: false,
|
|
2734
|
+
colName: pk
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
});
|
|
2738
|
+
return chunk2;
|
|
2739
|
+
});
|
|
2740
|
+
return columnsChunks.map((chunk2) => {
|
|
2741
|
+
const setValues = chunk2.map((column) => {
|
|
2742
|
+
if (column.isUnsafe) {
|
|
2743
|
+
return `\`destination\`.\`${columnUnsafeToSafeMapping[column.colName]}\` = \`sourced\`.\`${columnUnsafeToSafeMapping[column.colName]}\``;
|
|
2744
|
+
} else {
|
|
2745
|
+
return `\`destination\`.\`${column.colName}\` = \`sourced\`.\`${column.colName}\``;
|
|
2746
|
+
}
|
|
2747
|
+
}).join(`, `);
|
|
2748
|
+
const renamedCols = chunk2.map((column) => {
|
|
2749
|
+
if (column.isUnsafe) {
|
|
2750
|
+
return `\`${columnUnsafeToSafeMapping[column.colName]}\``;
|
|
2751
|
+
} else {
|
|
2752
|
+
return `\`${column.colName}\``;
|
|
2753
|
+
}
|
|
2754
|
+
}).join(`, `);
|
|
2755
|
+
const query = `
|
|
2756
|
+
MERGE ${this.sqlTableId} destination
|
|
2757
|
+
USING (
|
|
2758
|
+
WITH all_staging_rows AS (
|
|
2759
|
+
SELECT *
|
|
2760
|
+
FROM ${this.sqlStagingTableId}
|
|
2761
|
+
),
|
|
2762
|
+
numbered_rows AS (
|
|
2763
|
+
SELECT *,
|
|
2764
|
+
ROW_NUMBER() OVER (PARTITION BY ${primaryKeys}) as _wly_row_nb,
|
|
2765
|
+
COUNT(1) OVER (PARTITION BY ${primaryKeys}) AS _wly_partition_size,
|
|
2766
|
+
FROM all_staging_rows
|
|
2767
|
+
),
|
|
2768
|
+
staging_deduped AS (
|
|
2769
|
+
SELECT * EXCEPT(_wly_partition_size, _wly_row_nb)
|
|
2770
|
+
FROM numbered_rows
|
|
2771
|
+
WHERE _wly_partition_size = _wly_row_nb
|
|
2772
|
+
)
|
|
2773
|
+
SELECT *
|
|
2774
|
+
FROM staging_deduped
|
|
2775
|
+
) AS sourced
|
|
2776
|
+
ON ${primaryKeyCondition}
|
|
2777
|
+
WHEN MATCHED THEN
|
|
2778
|
+
UPDATE SET ${setValues}
|
|
2779
|
+
WHEN NOT MATCHED THEN
|
|
2780
|
+
INSERT (${renamedCols}) VALUES (${renamedCols});
|
|
2781
|
+
`;
|
|
2782
|
+
return query;
|
|
2783
|
+
});
|
|
2784
|
+
};
|
|
2785
|
+
getReplaceQueries = () => {
|
|
2786
|
+
return [
|
|
2787
|
+
`TRUNCATE TABLE ${this.sqlTableId};`,
|
|
2788
|
+
...this.getMergeQueries()
|
|
2789
|
+
];
|
|
2790
|
+
};
|
|
2791
|
+
createDatabaseAndSchemaIfNotExists = async (retryCount) => {
|
|
2792
|
+
try {
|
|
2793
|
+
await semaphore2.runExclusive(async () => {
|
|
2794
|
+
const [exists] = await this.bqDataset.exists();
|
|
2795
|
+
if (!exists) {
|
|
2796
|
+
logger.info(`\u{1F423} Creating dataset: ${this.bqDataset.id} on BigQuery`);
|
|
2797
|
+
await this.bqDataset.create();
|
|
2798
|
+
} else {
|
|
2799
|
+
const [metadata] = await this.bqDataset.getMetadata();
|
|
2800
|
+
const alreadyExistinglocation = metadata.location;
|
|
2801
|
+
if (this.config.location && this.config.location !== alreadyExistinglocation) {
|
|
2802
|
+
await haltAndCatchFire(
|
|
2803
|
+
`unknown`,
|
|
2804
|
+
`A BigQuery dataset called '${this.bqDataset.id}' already exists in location: '${alreadyExistinglocation}' \u{1F605}
|
|
2805
|
+
|
|
2806
|
+
This connector is configured to load data into the location '${this.config.location}' which is not possible as 2 datasets sharing the same name can't exist in different location on Google Cloud \u{1F63F}
|
|
2807
|
+
|
|
2808
|
+
To fix this issue, could you either: a. Delete the already existing dataset? b. Change the configured destination schema name to avoid the name conflict? \u{1F64F}
|
|
2809
|
+
|
|
2810
|
+
Please relaunch the connector once it's done to fix the issue \u{1F917}
|
|
2811
|
+
`,
|
|
2812
|
+
`
|
|
2813
|
+
We have a conflict between an already existing dataset in location ${alreadyExistinglocation} and the configured location: ${this.config.location}
|
|
2814
|
+
`
|
|
2815
|
+
);
|
|
2816
|
+
throw Error();
|
|
2817
|
+
}
|
|
2818
|
+
logger.info(`\u{1F44B} Dataset: ${this.bqDataset.id} already exists on BigQuery, reusing it.`);
|
|
2819
|
+
}
|
|
2820
|
+
});
|
|
2821
|
+
} catch (err) {
|
|
2822
|
+
if (err.code === 400) {
|
|
2823
|
+
await haltAndCatchFire(
|
|
2824
|
+
`unauthorized`,
|
|
2825
|
+
`We can't connect to your BigQuery account \u{1F614}
|
|
2826
|
+
|
|
2827
|
+
Here is the message from BigQuery: ${err.message}
|
|
2828
|
+
|
|
2829
|
+
Can you check that your Warehouse credentials and Source target schema are properly configured and try to sync again the source? \u{1F64F}
|
|
2830
|
+
`,
|
|
2831
|
+
`Got error when trying to get Dataset from BigQuery: ${err.message} - ${err.code}`
|
|
2832
|
+
);
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2835
|
+
if (retryCount && retryCount > this.maxRetryCount) {
|
|
2836
|
+
logger.error(`StreamId: ${this.streamId} - Error when checking if dataset already exists. ${err.message} - ${err.code}`);
|
|
2837
|
+
gracefulExit(1);
|
|
2838
|
+
} else {
|
|
2839
|
+
logger.error(`StreamId: ${this.streamId} - Error when checking if dataset already exists. Retrying... ${err.message} - ${err.code}`);
|
|
2840
|
+
await new Promise((resolve, reject) => setTimeout(resolve, 1e3 * retryCount));
|
|
2841
|
+
await this.createDatabaseAndSchemaIfNotExists(retryCount + 1);
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
return Promise.resolve();
|
|
2845
|
+
};
|
|
2846
|
+
createTable = async () => {
|
|
2847
|
+
try {
|
|
2848
|
+
const tableName = this.tableName;
|
|
2849
|
+
const schema = this.generateBigquerySchema();
|
|
2850
|
+
logger.info(`\u{1F423} Creating table: ${tableName} on BigQuery`);
|
|
2851
|
+
const createTableOptions = {
|
|
2852
|
+
schema
|
|
2853
|
+
};
|
|
2854
|
+
await this.bqDataset.createTable(
|
|
2855
|
+
tableName,
|
|
2856
|
+
createTableOptions
|
|
2857
|
+
);
|
|
2858
|
+
return;
|
|
2859
|
+
} catch (err) {
|
|
2860
|
+
throw new Error(`StreamId: ${this.streamId} - Issue when creating table in BigQuery
|
|
2861
|
+
|
|
2862
|
+
Error: ${err.message}
|
|
2863
|
+
Stack: ${err.stack}`);
|
|
2864
|
+
}
|
|
2865
|
+
};
|
|
2866
|
+
addColumns = async (tableFields) => {
|
|
2867
|
+
if (tableFields.length > 0) {
|
|
2868
|
+
logger.info(`\u{1F195} Stream: ${this.streamId} - Adding following columns in BigQuery schema: ${JSON.stringify(tableFields)}`);
|
|
2869
|
+
const [metadata] = await this.bqTable.getMetadata();
|
|
2870
|
+
const bgTableFields = tableFields.map((tblField) => {
|
|
2871
|
+
return this.getBigqueryTableFieldFromInternalSchemaFieldDefinition(tblField.name, tblField.definition);
|
|
2872
|
+
}).filter((fld) => {
|
|
2873
|
+
return !!fld;
|
|
2874
|
+
});
|
|
2875
|
+
const schema = metadata.schema;
|
|
2876
|
+
const new_schema = {
|
|
2877
|
+
...schema,
|
|
2878
|
+
fields: schema.fields.concat(bgTableFields)
|
|
2879
|
+
};
|
|
2880
|
+
metadata.schema = new_schema;
|
|
2881
|
+
await this.bqTable.setMetadata(metadata);
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
};
|
|
2885
|
+
createStagingArea = async () => {
|
|
2886
|
+
try {
|
|
2887
|
+
const tableName = this.stagingTableName;
|
|
2888
|
+
const [exists] = await this.bqDataset.table(tableName).exists();
|
|
2889
|
+
if (exists) {
|
|
2890
|
+
logger.info(`\u{1F474} Temporary table ${tableName} already exists, it should be a relic form the past.
|
|
2891
|
+
Dropping it.`);
|
|
2892
|
+
await this.deleteStagingArea();
|
|
2893
|
+
}
|
|
2894
|
+
const schema = this.generateBigquerySchema();
|
|
2895
|
+
const expirationTime = moment6().add("1", "day").valueOf().toString();
|
|
2896
|
+
const createTableOptions = {
|
|
2897
|
+
schema,
|
|
2898
|
+
expirationTime
|
|
2899
|
+
};
|
|
2900
|
+
logger.info(`\u{1F423} Creating staging table: ${tableName} on BigQuery`);
|
|
2901
|
+
await this.bqDataset.createTable(
|
|
2902
|
+
tableName,
|
|
2903
|
+
createTableOptions
|
|
2904
|
+
);
|
|
2905
|
+
return;
|
|
2906
|
+
} catch (err) {
|
|
2907
|
+
throw new Error(`StreamId: ${this.streamId} - Issue when creating table in BigQuery
|
|
2908
|
+
|
|
2909
|
+
Error: ${err.message}
|
|
2910
|
+
Stack: ${err.stack}`);
|
|
2911
|
+
}
|
|
2912
|
+
};
|
|
2913
|
+
loadStreamInStagingArea = async (localFilePath) => {
|
|
2914
|
+
try {
|
|
2915
|
+
const file = await uploadFileInBucket(
|
|
2916
|
+
this.streamId,
|
|
2917
|
+
this.config.loading_deck_gcs_bucket_name,
|
|
2918
|
+
localFilePath
|
|
2919
|
+
);
|
|
2920
|
+
const metadata = {
|
|
2921
|
+
sourceFormat: "NEWLINE_DELIMITED_JSON",
|
|
2922
|
+
writeDisposition: "WRITE_TRUNCATE"
|
|
2923
|
+
};
|
|
2924
|
+
await this.loadGCSFileInTable(this.sqlStagingTableId, file, metadata);
|
|
2925
|
+
} catch (err) {
|
|
2926
|
+
logger.error(`StreamId: ${this.streamId} - Error while uploading stream.`);
|
|
2927
|
+
throw err;
|
|
2928
|
+
}
|
|
2929
|
+
};
|
|
2930
|
+
deleteStagingArea = () => {
|
|
2931
|
+
const query = `DROP TABLE IF EXISTS ${this.sqlStagingTableId};`;
|
|
2932
|
+
return this.runQueriesWithRetry([query]);
|
|
2933
|
+
};
|
|
2934
|
+
getTablesInSchema = async () => {
|
|
2935
|
+
const [bqTables] = await this.bqDataset.getTables();
|
|
2936
|
+
const foundTables = bqTables.filter((tbl) => {
|
|
2937
|
+
return !!tbl.id;
|
|
2938
|
+
}).map((bqTable) => {
|
|
2939
|
+
return {
|
|
2940
|
+
tableName: bqTable.id
|
|
2941
|
+
};
|
|
2942
|
+
});
|
|
2943
|
+
return foundTables;
|
|
2944
|
+
};
|
|
2945
|
+
getTableColumnsFromWarehouse = async () => {
|
|
2946
|
+
const [table] = await this.bqTable.getMetadata();
|
|
2947
|
+
return table.schema.fields;
|
|
2948
|
+
};
|
|
2949
|
+
runQueries = async (queries) => {
|
|
2950
|
+
const sqlQuery = `
|
|
2951
|
+
BEGIN
|
|
2952
|
+
${queries.join(`
|
|
2953
|
+
`)}
|
|
2954
|
+
END;
|
|
2955
|
+
`;
|
|
2956
|
+
await this.bqClient.query(sqlQuery);
|
|
2957
|
+
};
|
|
2958
|
+
// Specific methods
|
|
2959
|
+
/**
|
|
2960
|
+
*
|
|
2961
|
+
* Generate a TableField definition object that can be passed to BigQuery APIs to define a column on a table
|
|
2962
|
+
*
|
|
2963
|
+
* @param colName
|
|
2964
|
+
* @param schemaFieldDefinition
|
|
2965
|
+
* @returns
|
|
2966
|
+
*/
|
|
2967
|
+
getBigqueryTableFieldFromInternalSchemaFieldDefinition = (colName, schemaFieldDefinition) => {
|
|
2968
|
+
const name = colName;
|
|
2969
|
+
if (!schemaFieldDefinition) {
|
|
2970
|
+
throw new Error(`StreamId: ${this.streamId} - Schema is unknown for colName=${colName}, so we can't infer the proper BigQuery definition for this column.`);
|
|
2971
|
+
}
|
|
2972
|
+
const type = schemaFieldDefinition.type;
|
|
2973
|
+
let typeArr;
|
|
2974
|
+
if (type instanceof Array) {
|
|
2975
|
+
typeArr = type;
|
|
2976
|
+
} else {
|
|
2977
|
+
typeArr = [type];
|
|
2978
|
+
}
|
|
2979
|
+
if (typeArr.includes("array")) {
|
|
2980
|
+
return {
|
|
2981
|
+
name,
|
|
2982
|
+
mode: "REPEATED",
|
|
2983
|
+
type: this.getWarehouseTypeFromJSONSchema(schemaFieldDefinition)
|
|
2984
|
+
};
|
|
2985
|
+
} else {
|
|
2986
|
+
let nullableMode = "NULLABLE";
|
|
2987
|
+
return {
|
|
2988
|
+
name,
|
|
2989
|
+
mode: nullableMode,
|
|
2990
|
+
type: this.getWarehouseTypeFromJSONSchema(schemaFieldDefinition)
|
|
2991
|
+
};
|
|
2992
|
+
}
|
|
2993
|
+
};
|
|
2994
|
+
generateBigquerySchema = () => {
|
|
2995
|
+
const unsafeToSafeMappingColumnsFromColumnStore = this.renamedColumnStore.getUnsafeToSafeColumnMapping(this.streamId);
|
|
2996
|
+
if (!unsafeToSafeMappingColumnsFromColumnStore) {
|
|
2997
|
+
throw new Error(`StreamId: ${this.streamId} - There is no unsafe->safe column mapping`);
|
|
2998
|
+
}
|
|
2999
|
+
return Object.keys(unsafeToSafeMappingColumnsFromColumnStore).reduce((acc, unsafeKey) => {
|
|
3000
|
+
const renamedColName = this.renamedColumnStore.getColumnTranslation(this.streamId, unsafeKey);
|
|
3001
|
+
const bigqueryTableField = this.getBigqueryTableFieldFromInternalSchemaFieldDefinition(renamedColName, this.flattenedSchema[unsafeKey]);
|
|
3002
|
+
if (bigqueryTableField) {
|
|
3003
|
+
acc.push(bigqueryTableField);
|
|
3004
|
+
}
|
|
3005
|
+
return acc;
|
|
3006
|
+
}, []);
|
|
3007
|
+
};
|
|
3008
|
+
loadGCSFileInTable = async (tableName, file, metadata) => {
|
|
3009
|
+
try {
|
|
3010
|
+
const [job] = await this.bqStagingTable.load(file, metadata);
|
|
3011
|
+
logger.info(`\u{1F69A} Loaded GCS file \`${file.id}\` in BigQuery table: ${tableName} with Job: \`${job.id}\``);
|
|
3012
|
+
logger.debug(`Upload Stats: ${JSON.stringify(job.statistics)}`);
|
|
3013
|
+
const errors2 = job.status?.errors;
|
|
3014
|
+
if (errors2 && errors2.length > 0) {
|
|
3015
|
+
throw errors2;
|
|
3016
|
+
}
|
|
3017
|
+
} catch (err) {
|
|
3018
|
+
logger.error(err);
|
|
3019
|
+
throw new Error(`StreamId: ${this.streamId} - Issue when loading file in BigQuery table | fileName: ${file.name}, tableName: ${tableName}
|
|
3020
|
+
|
|
3021
|
+
Error: ${err.message}
|
|
3022
|
+
Stack: ${err.stack}`);
|
|
3023
|
+
}
|
|
3024
|
+
};
|
|
3025
|
+
};
|
|
3026
|
+
|
|
3027
|
+
// src/targets/bigquery/main.ts
|
|
3028
|
+
var BigQueryTarget = class extends ITarget {
|
|
3029
|
+
constructor(config, resolver) {
|
|
3030
|
+
super(config, resolver);
|
|
3031
|
+
this.renameColumnStore.setSafeColumnNameConverter(this.safeColumnNameConverter);
|
|
3032
|
+
}
|
|
3033
|
+
static requiredConfigKeys = [
|
|
3034
|
+
"schema",
|
|
3035
|
+
"project_id",
|
|
3036
|
+
"loading_deck_gcs_bucket_name"
|
|
3037
|
+
];
|
|
3038
|
+
newDBSyncInstance = (streamId, database, schema, table) => {
|
|
3039
|
+
return new BigQueryDBSync(
|
|
3040
|
+
this.config,
|
|
3041
|
+
streamId,
|
|
3042
|
+
database,
|
|
3043
|
+
schema,
|
|
3044
|
+
table,
|
|
3045
|
+
this.renameColumnStore
|
|
3046
|
+
);
|
|
3047
|
+
};
|
|
3048
|
+
genTableName = safeTableName;
|
|
3049
|
+
safeColumnNameConverter = safeColumnName;
|
|
3050
|
+
validateDateRange = validateDateRange;
|
|
3051
|
+
convertNumberIntoDecimal = convertNumberIntoDecimal;
|
|
3052
|
+
};
|
|
2728
3053
|
export {
|
|
2729
3054
|
ALL_STREAM_ID_KEYWORD,
|
|
2730
3055
|
API_CALLS_METRIC_NAME,
|
|
2731
3056
|
Authenticator,
|
|
2732
3057
|
BATCH_INTERVAL_MS,
|
|
2733
|
-
|
|
3058
|
+
BigQueryTarget,
|
|
2734
3059
|
CounterMetric,
|
|
2735
3060
|
DEFAULT_MAX_CONCURRENT_STREAMS,
|
|
2736
3061
|
EXECUTION_TIME_METRIC_NAME,
|
|
2737
3062
|
ITarget,
|
|
2738
3063
|
MissingFieldInSchemaError,
|
|
2739
3064
|
MissingSchemaError,
|
|
2740
|
-
|
|
3065
|
+
RESTStream,
|
|
2741
3066
|
ROWS_SYNCED_METRIC_NAME,
|
|
2742
3067
|
RecordMessage,
|
|
2743
3068
|
RenameColumnStore,
|
|
@@ -2747,13 +3072,12 @@ export {
|
|
|
2747
3072
|
SchemaValidationError,
|
|
2748
3073
|
StateMessage,
|
|
2749
3074
|
StateService,
|
|
3075
|
+
Stream,
|
|
2750
3076
|
StreamMetadata,
|
|
2751
|
-
StreamV2,
|
|
2752
3077
|
StreamWarehouseSyncService,
|
|
2753
3078
|
Tap,
|
|
2754
3079
|
_greaterThan,
|
|
2755
3080
|
addWhalyFields,
|
|
2756
|
-
checkRequiredConfigKeys,
|
|
2757
3081
|
createTemporaryFileStream,
|
|
2758
3082
|
extractStateForStream,
|
|
2759
3083
|
finalizeStateProgressMarkers,
|
|
@@ -2771,8 +3095,8 @@ export {
|
|
|
2771
3095
|
haltAndCatchFire,
|
|
2772
3096
|
incrementStreamState,
|
|
2773
3097
|
loadJson,
|
|
3098
|
+
loadResolver,
|
|
2774
3099
|
loadSchema,
|
|
2775
|
-
loadShoreConfig,
|
|
2776
3100
|
logger,
|
|
2777
3101
|
postFormDataApiCall,
|
|
2778
3102
|
postJSONApiCall,
|
|
@@ -2780,7 +3104,6 @@ export {
|
|
|
2780
3104
|
printMemoryFootprint,
|
|
2781
3105
|
removeParasiteProperties,
|
|
2782
3106
|
safePath,
|
|
2783
|
-
writeCatalogFile,
|
|
2784
3107
|
writeStateFile
|
|
2785
3108
|
};
|
|
2786
3109
|
//# sourceMappingURL=index.mjs.map
|