airbyte-faros-destination 0.15.4 → 0.16.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/lib/index.d.ts +1 -0
- package/lib/index.js +15 -0
- package/lib/index.js.map +1 -1
- package/lib/testing-tools/cli.d.ts +37 -0
- package/lib/testing-tools/cli.js +86 -0
- package/lib/testing-tools/cli.js.map +1 -0
- package/lib/testing-tools/index.d.ts +3 -0
- package/lib/testing-tools/index.js +20 -0
- package/lib/testing-tools/index.js.map +1 -0
- package/lib/testing-tools/testing-tools.d.ts +36 -0
- package/lib/testing-tools/testing-tools.js +138 -0
- package/lib/testing-tools/testing-tools.js.map +1 -0
- package/lib/testing-tools/utils.d.ts +15 -0
- package/lib/testing-tools/utils.js +88 -0
- package/lib/testing-tools/utils.js.map +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
package/lib/index.d.ts
CHANGED
|
@@ -14,3 +14,4 @@ export declare function mainCommand(options?: {
|
|
|
14
14
|
export { Edition, InvalidRecordStrategy } from './common/types';
|
|
15
15
|
export { Converter, DestinationRecord, DestinationModel, StreamContext, StreamName, } from './converters/converter';
|
|
16
16
|
export { FarosDestinationRunner } from './destination-runner';
|
|
17
|
+
export * from './testing-tools/testing-tools';
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
2
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
17
|
exports.FarosDestinationRunner = exports.StreamName = exports.StreamContext = exports.Converter = exports.InvalidRecordStrategy = exports.Edition = void 0;
|
|
4
18
|
exports.mainCommand = mainCommand;
|
|
@@ -36,4 +50,5 @@ Object.defineProperty(exports, "StreamContext", { enumerable: true, get: functio
|
|
|
36
50
|
Object.defineProperty(exports, "StreamName", { enumerable: true, get: function () { return converter_1.StreamName; } });
|
|
37
51
|
var destination_runner_2 = require("./destination-runner");
|
|
38
52
|
Object.defineProperty(exports, "FarosDestinationRunner", { enumerable: true, get: function () { return destination_runner_2.FarosDestinationRunner; } });
|
|
53
|
+
__exportStar(require("./testing-tools/testing-tools"), exports);
|
|
39
54
|
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAYA,kCAoBC;AA9BD,6DAA4D;AAE5D;;;;;;;GAOG;AACH,SAAgB,WAAW,CAAC,OAG3B;IACC,MAAM,iBAAiB,GAAG,IAAI,2CAAsB,EAAE,CAAC;IACvD,MAAM,OAAO,GAAG,iBAAiB,CAAC,WAAW,EAAE,CAAC;IAEhD,IAAI,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,YAAY,EAAE,CAAC;QAC1B,OAAO,CAAC,YAAY,EAAE,CAAC;IACzB,CAAC;IACD,IAAI,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,cAAc,EAAE,CAAC;QAC5B,OAAO,CAAC,eAAe,CAAC;YACtB,gEAAgE;YAChE,QAAQ,EAAE,GAAG,EAAE,GAAE,CAAC;YAClB,gEAAgE;YAChE,QAAQ,EAAE,GAAG,EAAE,GAAE,CAAC;SACnB,CAAC,CAAC;IACL,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,wCAA8D;AAAtD,gGAAA,OAAO,OAAA;AAAE,8GAAA,qBAAqB,OAAA;AACtC,oDAMgC;AAL9B,sGAAA,SAAS,OAAA;AAGT,0GAAA,aAAa,OAAA;AACb,uGAAA,UAAU,OAAA;AAEZ,2DAA4D;AAApD,4HAAA,sBAAsB,OAAA;AAE9B,gEAA8C"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ChildProcess } from 'child_process';
|
|
2
|
+
import { Readable, Writable } from 'stream';
|
|
3
|
+
export declare function read(s: Readable): Promise<string>;
|
|
4
|
+
export declare function readLines(s: Readable): Promise<ReadonlyArray<string>>;
|
|
5
|
+
export interface CLIOptions {
|
|
6
|
+
env?: {
|
|
7
|
+
[key: string]: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/** A convenience class for testing CLI inputs and outputs. */
|
|
11
|
+
export declare class CLI {
|
|
12
|
+
private readonly cp;
|
|
13
|
+
constructor(cp: ChildProcess);
|
|
14
|
+
get stderr(): Readable;
|
|
15
|
+
get stdout(): Readable;
|
|
16
|
+
get stdin(): Writable;
|
|
17
|
+
/** Waits for the child process to terminate and returns the exit code. */
|
|
18
|
+
wait(): Promise<number>;
|
|
19
|
+
/**
|
|
20
|
+
* Checks stdin for the given prompt string, then writes the given response to
|
|
21
|
+
* stdout.
|
|
22
|
+
*/
|
|
23
|
+
respondToPrompt(prompt: string, response: string): void;
|
|
24
|
+
/**
|
|
25
|
+
* Runs CLI as a subprocess with the given arguments.
|
|
26
|
+
*
|
|
27
|
+
* If a home directory is not provided, the CLI's home directory will be set
|
|
28
|
+
* to an empty, unique, and temporary directory. In particular, this means
|
|
29
|
+
* that runs created by this function have separate caches and can run
|
|
30
|
+
* concurrently without interference.
|
|
31
|
+
*
|
|
32
|
+
* If a home diectory is not provided, the newly generated temporary directory
|
|
33
|
+
* will be deleted after the CLI command has returned. Otherwise, it is the
|
|
34
|
+
* caller's responsibility to clean up their chosen home directory.
|
|
35
|
+
*/
|
|
36
|
+
static runWith(args: string[], opts?: CLIOptions, bin?: string): Promise<CLI>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CLI = void 0;
|
|
7
|
+
exports.read = read;
|
|
8
|
+
exports.readLines = readLines;
|
|
9
|
+
const cross_spawn_1 = __importDefault(require("cross-spawn"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const consumers_1 = __importDefault(require("stream/consumers"));
|
|
12
|
+
async function read(s) {
|
|
13
|
+
return await consumers_1.default.text(s);
|
|
14
|
+
}
|
|
15
|
+
async function readLines(s) {
|
|
16
|
+
return (await read(s)).split('\n');
|
|
17
|
+
}
|
|
18
|
+
/** A convenience class for testing CLI inputs and outputs. */
|
|
19
|
+
class CLI {
|
|
20
|
+
constructor(cp) {
|
|
21
|
+
this.cp = cp;
|
|
22
|
+
}
|
|
23
|
+
get stderr() {
|
|
24
|
+
// `stderr` is always defined with the options in `runWith`.
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
26
|
+
return this.cp.stderr;
|
|
27
|
+
}
|
|
28
|
+
get stdout() {
|
|
29
|
+
// `stdout` is always defined with the options in `runWith`.
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
31
|
+
return this.cp.stdout;
|
|
32
|
+
}
|
|
33
|
+
get stdin() {
|
|
34
|
+
// `stdin` is always defined with the options in `runWith`.
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
36
|
+
return this.cp.stdin;
|
|
37
|
+
}
|
|
38
|
+
/** Waits for the child process to terminate and returns the exit code. */
|
|
39
|
+
async wait() {
|
|
40
|
+
if (this.cp.exitCode !== null) {
|
|
41
|
+
return this.cp.exitCode;
|
|
42
|
+
}
|
|
43
|
+
return new Promise((ok) => {
|
|
44
|
+
this.cp.on('exit', ok);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Checks stdin for the given prompt string, then writes the given response to
|
|
49
|
+
* stdout.
|
|
50
|
+
*/
|
|
51
|
+
respondToPrompt(prompt, response) {
|
|
52
|
+
// Use flag to only write to stdin once, since the Enquirer library prompt
|
|
53
|
+
// triggers stdout.on() multiple times.
|
|
54
|
+
let shouldRespond = true;
|
|
55
|
+
this.stdout.on('data', (data) => {
|
|
56
|
+
if (shouldRespond && data.toString().includes(prompt)) {
|
|
57
|
+
this.stdin.write(response, 'utf8');
|
|
58
|
+
shouldRespond = false;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Runs CLI as a subprocess with the given arguments.
|
|
64
|
+
*
|
|
65
|
+
* If a home directory is not provided, the CLI's home directory will be set
|
|
66
|
+
* to an empty, unique, and temporary directory. In particular, this means
|
|
67
|
+
* that runs created by this function have separate caches and can run
|
|
68
|
+
* concurrently without interference.
|
|
69
|
+
*
|
|
70
|
+
* If a home diectory is not provided, the newly generated temporary directory
|
|
71
|
+
* will be deleted after the CLI command has returned. Otherwise, it is the
|
|
72
|
+
* caller's responsibility to clean up their chosen home directory.
|
|
73
|
+
*/
|
|
74
|
+
static async runWith(args, opts = {}, bin = 'main') {
|
|
75
|
+
const spawnOpts = {
|
|
76
|
+
env: {
|
|
77
|
+
...process.env,
|
|
78
|
+
...opts.env,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
const cp = (0, cross_spawn_1.default)(path_1.default.resolve('bin', bin), args, spawnOpts);
|
|
82
|
+
return new CLI(cp);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
exports.CLI = CLI;
|
|
86
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/testing-tools/cli.ts"],"names":[],"mappings":";;;;;;AAMA,oBAEC;AAED,8BAEC;AAXD,8DAAgC;AAChC,gDAAwB;AAExB,iEAAyC;AAElC,KAAK,UAAU,IAAI,CAAC,CAAW;IACpC,OAAO,MAAM,mBAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjC,CAAC;AAEM,KAAK,UAAU,SAAS,CAAC,CAAW;IACzC,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACrC,CAAC;AAMD,8DAA8D;AAC9D,MAAa,GAAG;IACd,YAA6B,EAAgB;QAAhB,OAAE,GAAF,EAAE,CAAc;IAAG,CAAC;IAEjD,IAAI,MAAM;QACR,4DAA4D;QAC5D,oEAAoE;QACpE,OAAO,IAAI,CAAC,EAAE,CAAC,MAAO,CAAC;IACzB,CAAC;IAED,IAAI,MAAM;QACR,4DAA4D;QAC5D,oEAAoE;QACpE,OAAO,IAAI,CAAC,EAAE,CAAC,MAAO,CAAC;IACzB,CAAC;IAED,IAAI,KAAK;QACP,2DAA2D;QAC3D,oEAAoE;QACpE,OAAO,IAAI,CAAC,EAAE,CAAC,KAAM,CAAC;IACxB,CAAC;IAED,0EAA0E;IAC1E,KAAK,CAAC,IAAI;QACR,IAAI,IAAI,CAAC,EAAE,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;YAC9B,OAAO,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC;QAC1B,CAAC;QACD,OAAO,IAAI,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE;YACxB,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACzB,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;OAGG;IACH,eAAe,CAAC,MAAc,EAAE,QAAgB;QAC9C,0EAA0E;QAC1E,uCAAuC;QACvC,IAAI,aAAa,GAAG,IAAI,CAAC;QACzB,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAS,EAAE,EAAE;YACnC,IAAI,aAAa,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBACtD,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;gBACnC,aAAa,GAAG,KAAK,CAAC;YACxB,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,KAAK,CAAC,OAAO,CAClB,IAAc,EACd,OAAmB,EAAE,EACrB,GAAG,GAAG,MAAM;QAEZ,MAAM,SAAS,GAAG;YAChB,GAAG,EAAE;gBACH,GAAG,OAAO,CAAC,GAAG;gBACd,GAAG,IAAI,CAAC,GAAG;aACZ;SACF,CAAC;QACF,MAAM,EAAE,GAAG,IAAA,qBAAK,EAAC,cAAI,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;QAC5D,OAAO,IAAI,GAAG,CAAC,EAAE,CAAC,CAAC;IACrB,CAAC;CACF;AAzED,kBAyEC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./cli"), exports);
|
|
18
|
+
__exportStar(require("./testing-tools"), exports);
|
|
19
|
+
__exportStar(require("./utils"), exports);
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing-tools/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,wCAAsB;AACtB,kDAAgC;AAChC,0CAAwB"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Mockttp } from 'mockttp';
|
|
2
|
+
import pino from 'pino';
|
|
3
|
+
import { Dictionary } from 'ts-essentials';
|
|
4
|
+
import { Edition, InvalidRecordStrategy } from '../common/types';
|
|
5
|
+
/**
|
|
6
|
+
* Read a test resource by name
|
|
7
|
+
*/
|
|
8
|
+
export declare function readTestResourceFile(fileName: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Parse a test resource into JSON
|
|
11
|
+
*/
|
|
12
|
+
export declare function readTestResourceAsJSON(fileName: string): any;
|
|
13
|
+
/**
|
|
14
|
+
* Creates a temporary file
|
|
15
|
+
* @return path to the temporary file
|
|
16
|
+
*/
|
|
17
|
+
export declare function tempFile(data: string, postfix: string): Promise<string>;
|
|
18
|
+
export interface TempConfigOptions {
|
|
19
|
+
readonly api_url: string;
|
|
20
|
+
readonly invalid_record_strategy?: InvalidRecordStrategy;
|
|
21
|
+
readonly edition?: Edition;
|
|
22
|
+
readonly edition_configs?: Dictionary<any>;
|
|
23
|
+
readonly source_specific_configs?: Dictionary<any>;
|
|
24
|
+
readonly replace_origin_map?: Dictionary<any>;
|
|
25
|
+
readonly exclude_fields_map?: Dictionary<any>;
|
|
26
|
+
readonly log_records?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Creates a temporary file with testing configuration
|
|
30
|
+
* @return path to the temporary config file to delete
|
|
31
|
+
*/
|
|
32
|
+
export declare function tempConfig(options: TempConfigOptions): Promise<string>;
|
|
33
|
+
export declare function getConf(options: TempConfigOptions): any;
|
|
34
|
+
export declare function sourceSpecificTempConfig(url: string, source_specific_configs: Dictionary<any>): Promise<string>;
|
|
35
|
+
export declare function initMockttp(mockttp: Mockttp): Promise<void>;
|
|
36
|
+
export declare function testLogger(name?: string): pino.Logger;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.readTestResourceFile = readTestResourceFile;
|
|
7
|
+
exports.readTestResourceAsJSON = readTestResourceAsJSON;
|
|
8
|
+
exports.tempFile = tempFile;
|
|
9
|
+
exports.tempConfig = tempConfig;
|
|
10
|
+
exports.getConf = getConf;
|
|
11
|
+
exports.sourceSpecificTempConfig = sourceSpecificTempConfig;
|
|
12
|
+
exports.initMockttp = initMockttp;
|
|
13
|
+
exports.testLogger = testLogger;
|
|
14
|
+
const fs_1 = __importDefault(require("fs"));
|
|
15
|
+
const pino_1 = __importDefault(require("pino"));
|
|
16
|
+
const tmp_promise_1 = __importDefault(require("tmp-promise"));
|
|
17
|
+
const util_1 = __importDefault(require("util"));
|
|
18
|
+
const types_1 = require("../common/types");
|
|
19
|
+
const TEST_SOURCE_ID = 'mytestsource';
|
|
20
|
+
// Remove all controlled temporary objects on process exit
|
|
21
|
+
tmp_promise_1.default.setGracefulCleanup();
|
|
22
|
+
/**
|
|
23
|
+
* Read a test resource by name
|
|
24
|
+
*/
|
|
25
|
+
function readTestResourceFile(fileName) {
|
|
26
|
+
return fs_1.default.readFileSync(`test/resources/${fileName}`, 'utf8');
|
|
27
|
+
}
|
|
28
|
+
const writeFile = util_1.default.promisify(fs_1.default.write);
|
|
29
|
+
/**
|
|
30
|
+
* Parse a test resource into JSON
|
|
31
|
+
*/
|
|
32
|
+
function readTestResourceAsJSON(fileName) {
|
|
33
|
+
return JSON.parse(readTestResourceFile(fileName));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Creates a temporary file
|
|
37
|
+
* @return path to the temporary file
|
|
38
|
+
*/
|
|
39
|
+
async function tempFile(data, postfix) {
|
|
40
|
+
const file = await tmp_promise_1.default.file({ postfix });
|
|
41
|
+
await writeFile(file.fd, data, null, 'utf-8');
|
|
42
|
+
return file.path;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Creates a temporary file with testing configuration
|
|
46
|
+
* @return path to the temporary config file to delete
|
|
47
|
+
*/
|
|
48
|
+
async function tempConfig(options) {
|
|
49
|
+
const conf = getConf(options);
|
|
50
|
+
return tempFile(JSON.stringify(conf), '.json');
|
|
51
|
+
}
|
|
52
|
+
function getConf(options) {
|
|
53
|
+
const { api_url, invalid_record_strategy = types_1.InvalidRecordStrategy.FAIL, edition = types_1.Edition.CLOUD, edition_configs = {}, source_specific_configs = {}, replace_origin_map = {}, exclude_fields_map = {}, log_records = false, } = options;
|
|
54
|
+
const edition_configs_defaults = edition === types_1.Edition.CLOUD
|
|
55
|
+
? {
|
|
56
|
+
edition,
|
|
57
|
+
api_url,
|
|
58
|
+
api_key: 'test-api-key',
|
|
59
|
+
graph: 'test-graph',
|
|
60
|
+
}
|
|
61
|
+
: {
|
|
62
|
+
edition,
|
|
63
|
+
hasura_url: api_url,
|
|
64
|
+
segment_user_id: 'bacaf6e6-41d8-4102-a3a4-5d28100e642f',
|
|
65
|
+
segment_test_host: api_url,
|
|
66
|
+
};
|
|
67
|
+
const conf = {
|
|
68
|
+
edition_configs: { ...edition_configs_defaults, ...edition_configs },
|
|
69
|
+
invalid_record_strategy,
|
|
70
|
+
origin: 'test-origin',
|
|
71
|
+
jsonata_destination_models: ['generic_Record'],
|
|
72
|
+
jsonata_expression: `
|
|
73
|
+
data.{
|
|
74
|
+
"model": "generic_Record",
|
|
75
|
+
"record": {
|
|
76
|
+
"uid": foo
|
|
77
|
+
}
|
|
78
|
+
}`,
|
|
79
|
+
source_specific_configs,
|
|
80
|
+
replace_origin_map: JSON.stringify(replace_origin_map),
|
|
81
|
+
exclude_fields_map: JSON.stringify(exclude_fields_map),
|
|
82
|
+
faros_source_id: TEST_SOURCE_ID,
|
|
83
|
+
log_records,
|
|
84
|
+
};
|
|
85
|
+
return conf;
|
|
86
|
+
}
|
|
87
|
+
function sourceSpecificTempConfig(url, source_specific_configs) {
|
|
88
|
+
return tempConfig({ api_url: url, source_specific_configs });
|
|
89
|
+
}
|
|
90
|
+
async function initMockttp(mockttp) {
|
|
91
|
+
await mockttp.start({ startPort: 30000, endPort: 50000 });
|
|
92
|
+
// Faros GraphQL auth check
|
|
93
|
+
await mockttp
|
|
94
|
+
.forGet('/users/me')
|
|
95
|
+
.once()
|
|
96
|
+
.thenReply(200, JSON.stringify({ tenantId: '1' }));
|
|
97
|
+
// Faros GraphQL graph stats
|
|
98
|
+
await mockttp
|
|
99
|
+
.forGet('/graphs/test-graph/statistics')
|
|
100
|
+
.once()
|
|
101
|
+
.thenReply(200, JSON.stringify({}));
|
|
102
|
+
// Hasura health check
|
|
103
|
+
await mockttp.forGet('/healthz').once().thenReply(200, JSON.stringify({}));
|
|
104
|
+
// Get Faros Account
|
|
105
|
+
await mockttp
|
|
106
|
+
.forGet(`/accounts/${TEST_SOURCE_ID}`)
|
|
107
|
+
.once()
|
|
108
|
+
.thenReply(200, JSON.stringify({ account: { accountId: TEST_SOURCE_ID } }));
|
|
109
|
+
// Faros Account Sync
|
|
110
|
+
const mockSyncResult = {
|
|
111
|
+
sync: {
|
|
112
|
+
syncId: '1',
|
|
113
|
+
logId: '1',
|
|
114
|
+
startedAt: new Date().toISOString(),
|
|
115
|
+
status: 'running',
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
await mockttp
|
|
119
|
+
.forPut(`/accounts/${TEST_SOURCE_ID}/syncs`)
|
|
120
|
+
.once()
|
|
121
|
+
.thenReply(200, JSON.stringify(mockSyncResult));
|
|
122
|
+
await mockttp
|
|
123
|
+
.forPatch(`/accounts/${TEST_SOURCE_ID}/syncs/1`)
|
|
124
|
+
.once()
|
|
125
|
+
.thenReply(200, JSON.stringify(mockSyncResult));
|
|
126
|
+
}
|
|
127
|
+
function testLogger(name = 'test') {
|
|
128
|
+
var _a, _b;
|
|
129
|
+
return (0, pino_1.default)({
|
|
130
|
+
name,
|
|
131
|
+
level: (_a = process.env.LOG_LEVEL) !== null && _a !== void 0 ? _a : 'info',
|
|
132
|
+
// pino-pretty leaves threads open which can prevent Jest from exiting properly
|
|
133
|
+
transport: ((_b = process.env.LOG_LEVEL) === null || _b === void 0 ? void 0 : _b.toLowerCase()) === 'debug'
|
|
134
|
+
? { target: 'pino-pretty', options: { levelFirst: true } }
|
|
135
|
+
: undefined,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=testing-tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing-tools.js","sourceRoot":"","sources":["../../src/testing-tools/testing-tools.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Dictionary } from 'ts-essentials';
|
|
2
|
+
export interface DestinationWriteTestOptions {
|
|
3
|
+
configPath: string;
|
|
4
|
+
catalogPath: string;
|
|
5
|
+
inputRecordsPath: string;
|
|
6
|
+
checkRecordsData?: (records: ReadonlyArray<Dictionary<any>>) => void;
|
|
7
|
+
}
|
|
8
|
+
export interface GenerateBasicTestSuiteOptions {
|
|
9
|
+
sourceName: string;
|
|
10
|
+
catalogPath?: string;
|
|
11
|
+
inputRecordsPath?: string;
|
|
12
|
+
checkRecordsData?: (records: ReadonlyArray<Dictionary<any>>) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare const destinationWriteTest: (options: DestinationWriteTestOptions) => Promise<void>;
|
|
15
|
+
export declare function generateBasicTestSuite({ sourceName, catalogPath, inputRecordsPath, checkRecordsData, }: GenerateBasicTestSuiteOptions): void;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.destinationWriteTest = void 0;
|
|
4
|
+
exports.generateBasicTestSuite = generateBasicTestSuite;
|
|
5
|
+
const faros_airbyte_cdk_1 = require("faros-airbyte-cdk");
|
|
6
|
+
const mockttp_1 = require("mockttp");
|
|
7
|
+
const cli_1 = require("./cli");
|
|
8
|
+
const testing_tools_1 = require("./testing-tools");
|
|
9
|
+
// Executes the destination write command in dry-run mode and optionally checks:
|
|
10
|
+
// - The processed and written records count
|
|
11
|
+
// - The records data
|
|
12
|
+
const destinationWriteTest = async (options) => {
|
|
13
|
+
const { configPath, catalogPath, inputRecordsPath, checkRecordsData = undefined, } = options;
|
|
14
|
+
const cli = await cli_1.CLI.runWith([
|
|
15
|
+
'write',
|
|
16
|
+
'--config',
|
|
17
|
+
configPath,
|
|
18
|
+
'--catalog',
|
|
19
|
+
catalogPath,
|
|
20
|
+
'--dry-run',
|
|
21
|
+
]);
|
|
22
|
+
cli.stdin.end((0, testing_tools_1.readTestResourceFile)(inputRecordsPath), 'utf8');
|
|
23
|
+
const stdoutLines = await (0, cli_1.readLines)(cli.stdout);
|
|
24
|
+
const matches = [];
|
|
25
|
+
stdoutLines.forEach((line) => {
|
|
26
|
+
const regexes = [
|
|
27
|
+
/Processed (\d+) records/,
|
|
28
|
+
/Would write (\d+) records/,
|
|
29
|
+
/Errored (\d+) records/,
|
|
30
|
+
/Skipped (\d+) records/,
|
|
31
|
+
/Processed records by stream: {(.*)}/,
|
|
32
|
+
/Would write records by model: {(.*)}/,
|
|
33
|
+
];
|
|
34
|
+
const matchedLine = regexes.find((regex) => regex.test(line));
|
|
35
|
+
if (matchedLine) {
|
|
36
|
+
matches.push(matchedLine.exec(line)[0]);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
expect(matches).toMatchSnapshot();
|
|
40
|
+
if (checkRecordsData) {
|
|
41
|
+
const records = readRecordData(stdoutLines);
|
|
42
|
+
checkRecordsData(records);
|
|
43
|
+
}
|
|
44
|
+
expect(await (0, cli_1.read)(cli.stderr)).toBe('');
|
|
45
|
+
expect(await cli.wait()).toBe(0);
|
|
46
|
+
};
|
|
47
|
+
exports.destinationWriteTest = destinationWriteTest;
|
|
48
|
+
function readRecordData(lines) {
|
|
49
|
+
const records = [];
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
try {
|
|
52
|
+
const msg = (0, faros_airbyte_cdk_1.parseAirbyteMessage)(line);
|
|
53
|
+
if (msg.type === faros_airbyte_cdk_1.AirbyteMessageType.RECORD) {
|
|
54
|
+
const recordMessage = msg;
|
|
55
|
+
records.push(recordMessage.record.data);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
// Not a record message. Ignore.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return records;
|
|
63
|
+
}
|
|
64
|
+
function generateBasicTestSuite({ sourceName, catalogPath = `test/resources/${sourceName}/catalog.json`, inputRecordsPath = `${sourceName}/all-streams.log`, checkRecordsData, }) {
|
|
65
|
+
describe(`${sourceName} basic test`, () => {
|
|
66
|
+
const mockttp = (0, mockttp_1.getLocal)({ debug: false, recordTraffic: false });
|
|
67
|
+
let configPath;
|
|
68
|
+
beforeEach(async () => {
|
|
69
|
+
await (0, testing_tools_1.initMockttp)(mockttp);
|
|
70
|
+
configPath = await (0, testing_tools_1.tempConfig)({
|
|
71
|
+
api_url: mockttp.url,
|
|
72
|
+
log_records: !!checkRecordsData,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
afterEach(async () => {
|
|
76
|
+
await mockttp.stop();
|
|
77
|
+
});
|
|
78
|
+
test('process records from all streams', async () => {
|
|
79
|
+
await (0, exports.destinationWriteTest)({
|
|
80
|
+
configPath,
|
|
81
|
+
catalogPath,
|
|
82
|
+
inputRecordsPath,
|
|
83
|
+
checkRecordsData,
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/testing-tools/utils.ts"],"names":[],"mappings":"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"}
|