@miso.ai/server-sdk 0.6.6-beta.6 → 0.6.6
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/cli/delete.js +34 -20
- package/cli/get.js +2 -1
- package/cli/hybrid-search.js +4 -3
- package/cli/ids-diff.js +4 -2
- package/cli/ids.js +4 -2
- package/cli/index.js +2 -17
- package/cli/merge.js +5 -4
- package/cli/search.js +4 -3
- package/cli/status.js +5 -3
- package/cli/transform.js +9 -12
- package/cli/upgrade.js +37 -0
- package/cli/upload.js +41 -29
- package/cli/utils.js +36 -3
- package/package.json +2 -2
- package/src/api/base.js +10 -1
- package/src/api/helpers.js +13 -10
- package/src/api/index.js +0 -2
- package/src/channel/api.js +194 -0
- package/src/channel/delete.js +68 -0
- package/src/channel/index.js +2 -0
- package/src/channel/upload.js +68 -0
- package/src/client.js +8 -0
- package/src/stream/api-sink.js +3 -1
- package/src/stream/delete.js +4 -0
- package/src/stream/upload-sink.js +0 -16
- package/src/stream/upload.js +3 -5
- package/src/version.js +1 -1
- package/src/api/experiments.js +0 -19
package/cli/delete.js
CHANGED
|
@@ -1,21 +1,11 @@
|
|
|
1
|
+
import { pipeline } from 'stream/promises';
|
|
1
2
|
import split2 from 'split2';
|
|
2
|
-
import { log, stream } from '@miso.ai/server-commons';
|
|
3
|
+
import { log, stream, splitObj } from '@miso.ai/server-commons';
|
|
3
4
|
import { MisoClient, logger } from '../src/index.js';
|
|
5
|
+
import { buildForWrite } from './utils.js';
|
|
4
6
|
|
|
5
7
|
function build(yargs) {
|
|
6
|
-
return yargs
|
|
7
|
-
.option('records-per-request', {
|
|
8
|
-
alias: ['rpr'],
|
|
9
|
-
describe: 'How many records to send in a request',
|
|
10
|
-
})
|
|
11
|
-
.option('records-per-second', {
|
|
12
|
-
alias: ['rps'],
|
|
13
|
-
describe: 'How many records to send per second',
|
|
14
|
-
})
|
|
15
|
-
.option('debug', {
|
|
16
|
-
describe: 'Set log level to debug',
|
|
17
|
-
type: 'boolean',
|
|
18
|
-
})
|
|
8
|
+
return buildForWrite(yargs)
|
|
19
9
|
.option('progress', {
|
|
20
10
|
alias: ['p'],
|
|
21
11
|
describe: 'Set log format progress',
|
|
@@ -34,8 +24,35 @@ function build(yargs) {
|
|
|
34
24
|
}
|
|
35
25
|
|
|
36
26
|
const run = type => async ({
|
|
27
|
+
env,
|
|
37
28
|
key,
|
|
38
29
|
server,
|
|
30
|
+
channel,
|
|
31
|
+
...options
|
|
32
|
+
}) => {
|
|
33
|
+
const { debug } = options;
|
|
34
|
+
const client = new MisoClient({ env, key, server, debug });
|
|
35
|
+
|
|
36
|
+
if (channel) {
|
|
37
|
+
await runChannel(client, type, options);
|
|
38
|
+
} else {
|
|
39
|
+
await runStream(client, type, options);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
async function runChannel(client, type, options) {
|
|
44
|
+
const [deleteOptions] = splitObj(options, ['params', 'requestsPerSecond', 'bytesPerSecond', 'recordsPerRequest', 'bytesPerRequest', 'debug']);
|
|
45
|
+
const deleteChannel = client.api[type].deleteChannel(deleteOptions);
|
|
46
|
+
|
|
47
|
+
await pipeline(
|
|
48
|
+
process.stdin,
|
|
49
|
+
split2(JSON.parse),
|
|
50
|
+
deleteChannel,
|
|
51
|
+
new stream.OutputStream({ objectMode: true }),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function runStream(client, type, {
|
|
39
56
|
param: params,
|
|
40
57
|
['records-per-request']: recordsPerRequest,
|
|
41
58
|
['records-per-second']: recordsPerSecond,
|
|
@@ -44,13 +61,10 @@ const run = type => async ({
|
|
|
44
61
|
['stream-name']: name,
|
|
45
62
|
['log-level']: loglevel,
|
|
46
63
|
['log-format']: logFormat,
|
|
47
|
-
})
|
|
48
|
-
|
|
64
|
+
}) {
|
|
49
65
|
loglevel = (debug || progress) ? log.DEBUG : loglevel;
|
|
50
66
|
logFormat = progress ? logger.FORMAT.PROGRESS : logFormat;
|
|
51
67
|
|
|
52
|
-
const client = new MisoClient({ key, server, debug });
|
|
53
|
-
|
|
54
68
|
const deleteStream = client.api[type].deleteStream({
|
|
55
69
|
name,
|
|
56
70
|
params,
|
|
@@ -66,13 +80,13 @@ const run = type => async ({
|
|
|
66
80
|
format: logFormat,
|
|
67
81
|
});
|
|
68
82
|
|
|
69
|
-
await
|
|
83
|
+
await pipeline(
|
|
70
84
|
process.stdin,
|
|
71
85
|
split2(),
|
|
72
86
|
deleteStream,
|
|
73
87
|
logStream,
|
|
74
88
|
);
|
|
75
|
-
}
|
|
89
|
+
}
|
|
76
90
|
|
|
77
91
|
export default function(type) {
|
|
78
92
|
return {
|
package/cli/get.js
CHANGED
|
@@ -4,12 +4,13 @@ import { formatError } from './utils.js';
|
|
|
4
4
|
const build = yargs => yargs;
|
|
5
5
|
|
|
6
6
|
const run = type => async ({
|
|
7
|
+
env,
|
|
7
8
|
key,
|
|
8
9
|
server,
|
|
9
10
|
id,
|
|
10
11
|
debug,
|
|
11
12
|
}) => {
|
|
12
|
-
const client = new MisoClient({ key, server, debug });
|
|
13
|
+
const client = new MisoClient({ env, key, server, debug });
|
|
13
14
|
try {
|
|
14
15
|
const entity = await client.api[type].get(id);
|
|
15
16
|
console.log(JSON.stringify(entity));
|
package/cli/hybrid-search.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Readable } from 'stream';
|
|
2
|
+
import { pipeline } from 'stream/promises';
|
|
2
3
|
import { stream } from '@miso.ai/server-commons';
|
|
3
4
|
import { MisoClient } from '../src/index.js';
|
|
4
5
|
import { buildForApi, buildForSearch } from './utils.js';
|
|
@@ -15,13 +16,13 @@ function build(yargs) {
|
|
|
15
16
|
});
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
async function run({ query, fq, fl, rows, answer, key, server, debug }) {
|
|
19
|
-
const client = new MisoClient({ key, server, debug });
|
|
19
|
+
async function run({ query, fq, fl, rows, answer, env, key, server, debug }) {
|
|
20
|
+
const client = new MisoClient({ env, key, server, debug });
|
|
20
21
|
const { products } = await client.api.ask.search({ q: query, fq, fl, rows, answer });
|
|
21
22
|
const readStream = Readable.from(products);
|
|
22
23
|
const outputStream = new stream.OutputStream();
|
|
23
24
|
|
|
24
|
-
await
|
|
25
|
+
await pipeline(
|
|
25
26
|
readStream,
|
|
26
27
|
outputStream,
|
|
27
28
|
);
|
package/cli/ids-diff.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { pipeline } from 'stream/promises';
|
|
1
2
|
import split2 from 'split2';
|
|
2
3
|
import { stream } from '@miso.ai/server-commons';
|
|
3
4
|
import { MisoClient } from '../src/index.js';
|
|
@@ -25,6 +26,7 @@ function build(yargs) {
|
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
const run = type => async ({
|
|
29
|
+
env,
|
|
28
30
|
key,
|
|
29
31
|
server,
|
|
30
32
|
output,
|
|
@@ -34,13 +36,13 @@ const run = type => async ({
|
|
|
34
36
|
}) => {
|
|
35
37
|
output = output || (plus ? 'plus' : minus ? 'minus' : undefined);
|
|
36
38
|
|
|
37
|
-
const client = new MisoClient({ key, server, debug });
|
|
39
|
+
const client = new MisoClient({ env, key, server, debug });
|
|
38
40
|
const misoIds = await client.api[type].ids();
|
|
39
41
|
|
|
40
42
|
const diffStream = new stream.DiffStream(misoIds, { output });
|
|
41
43
|
const outputStream = new stream.OutputStream({ objectMode: false });
|
|
42
44
|
|
|
43
|
-
await
|
|
45
|
+
await pipeline(
|
|
44
46
|
process.stdin,
|
|
45
47
|
split2(),
|
|
46
48
|
diffStream,
|
package/cli/ids.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Readable } from 'stream';
|
|
2
|
+
import { pipeline } from 'stream/promises';
|
|
2
3
|
import { stream } from '@miso.ai/server-commons';
|
|
3
4
|
import { MisoClient } from '../src/index.js';
|
|
4
5
|
import diff from './ids-diff.js';
|
|
@@ -19,12 +20,13 @@ const build = type => yargs => {
|
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
const run = type => async ({
|
|
23
|
+
env,
|
|
22
24
|
key,
|
|
23
25
|
server,
|
|
24
26
|
type: recordType,
|
|
25
27
|
debug,
|
|
26
28
|
}) => {
|
|
27
|
-
const client = new MisoClient({ key, server, debug });
|
|
29
|
+
const client = new MisoClient({ env, key, server, debug });
|
|
28
30
|
let ids;
|
|
29
31
|
try {
|
|
30
32
|
const options = recordType ? { type: recordType } : {};
|
|
@@ -37,7 +39,7 @@ const run = type => async ({
|
|
|
37
39
|
const readStream = Readable.from(ids);
|
|
38
40
|
const outputStream = new stream.OutputStream();
|
|
39
41
|
|
|
40
|
-
await
|
|
42
|
+
await pipeline(
|
|
41
43
|
readStream,
|
|
42
44
|
outputStream,
|
|
43
45
|
);
|
package/cli/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import status from './status.js';
|
|
|
12
12
|
import get from './get.js';
|
|
13
13
|
import search from './search.js';
|
|
14
14
|
import hybridSearch from './hybrid-search.js';
|
|
15
|
+
import upgrade from './upgrade.js';
|
|
15
16
|
|
|
16
17
|
const interactions = {
|
|
17
18
|
command: 'interactions',
|
|
@@ -47,30 +48,14 @@ const users = {
|
|
|
47
48
|
.command(status('users')),
|
|
48
49
|
};
|
|
49
50
|
|
|
50
|
-
const experiments = {
|
|
51
|
-
command: 'experiments',
|
|
52
|
-
aliases: ['experiment'],
|
|
53
|
-
description: 'Experiment commands',
|
|
54
|
-
builder: yargs => buildForApi(yargs)
|
|
55
|
-
.option('experiment-id', {
|
|
56
|
-
alias: ['exp-id'],
|
|
57
|
-
describe: 'Experiment ID for experiment API',
|
|
58
|
-
})
|
|
59
|
-
.command({
|
|
60
|
-
command: 'events',
|
|
61
|
-
builder: yargs => yargs
|
|
62
|
-
.command(upload('experiment-events')),
|
|
63
|
-
}),
|
|
64
|
-
};
|
|
65
|
-
|
|
66
51
|
yargs.build(yargs => {
|
|
67
52
|
yargs
|
|
68
53
|
.env('MISO')
|
|
69
54
|
.command(interactions)
|
|
70
55
|
.command(products)
|
|
71
56
|
.command(users)
|
|
72
|
-
.command(experiments)
|
|
73
57
|
.command(transform)
|
|
58
|
+
.command(upgrade)
|
|
74
59
|
.command(mergeLocal)
|
|
75
60
|
.command(search)
|
|
76
61
|
.command(hybridSearch)
|
package/cli/merge.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from 'path';
|
|
2
2
|
import { createReadStream } from 'fs';
|
|
3
3
|
import { createGunzip } from 'zlib';
|
|
4
|
+
import { pipeline } from 'stream/promises';
|
|
4
5
|
import split2 from 'split2';
|
|
5
6
|
import { stream } from '@miso.ai/server-commons';
|
|
6
7
|
import { MisoClient } from '../src/index.js';
|
|
@@ -23,6 +24,7 @@ function build(yargs) {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const run = type => async ({
|
|
27
|
+
env,
|
|
26
28
|
key,
|
|
27
29
|
server,
|
|
28
30
|
file,
|
|
@@ -32,13 +34,12 @@ const run = type => async ({
|
|
|
32
34
|
}) => {
|
|
33
35
|
const mergeFn = await getMergeFn(file);
|
|
34
36
|
const records = await buildBaseRecords(base);
|
|
35
|
-
const client = new MisoClient({ key, server, debug });
|
|
37
|
+
const client = new MisoClient({ env, key, server, debug });
|
|
36
38
|
const mergeStream = client.api[type].mergeStream({ ...options, mergeFn, records });
|
|
37
39
|
const outputStream = new stream.OutputStream({ objectMode: true });
|
|
38
|
-
await
|
|
40
|
+
await pipeline(
|
|
39
41
|
process.stdin,
|
|
40
|
-
split2(),
|
|
41
|
-
stream.parse(),
|
|
42
|
+
split2(JSON.parse),
|
|
42
43
|
mergeStream,
|
|
43
44
|
outputStream
|
|
44
45
|
);
|
package/cli/search.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Readable } from 'stream';
|
|
2
|
+
import { pipeline } from 'stream/promises';
|
|
2
3
|
import { stream } from '@miso.ai/server-commons';
|
|
3
4
|
import { MisoClient } from '../src/index.js';
|
|
4
5
|
import { buildForApi, buildForSearch } from './utils.js';
|
|
@@ -10,13 +11,13 @@ function build(yargs) {
|
|
|
10
11
|
});
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
async function run({ query, fq, fl, rows, key, server, debug }) {
|
|
14
|
-
const client = new MisoClient({ key, server, debug });
|
|
14
|
+
async function run({ query, fq, fl, rows, env, key, server, debug }) {
|
|
15
|
+
const client = new MisoClient({ env, key, server, debug });
|
|
15
16
|
const records = await client.api.search.search({ q: query, fq, fl, rows });
|
|
16
17
|
const readStream = Readable.from(records);
|
|
17
18
|
const outputStream = new stream.OutputStream();
|
|
18
19
|
|
|
19
|
-
await
|
|
20
|
+
await pipeline(
|
|
20
21
|
readStream,
|
|
21
22
|
outputStream,
|
|
22
23
|
);
|
package/cli/status.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { pipeline } from 'stream/promises';
|
|
1
2
|
import split2 from 'split2';
|
|
2
3
|
import { stream } from '@miso.ai/server-commons';
|
|
3
4
|
import { MisoClient } from '../src/index.js';
|
|
@@ -7,12 +8,13 @@ const build = type => yargs => {
|
|
|
7
8
|
};
|
|
8
9
|
|
|
9
10
|
const run = type => async ({
|
|
11
|
+
env,
|
|
10
12
|
key,
|
|
11
13
|
server,
|
|
12
14
|
taskId,
|
|
13
15
|
debug,
|
|
14
16
|
}) => {
|
|
15
|
-
const client = new MisoClient({ key, server, debug });
|
|
17
|
+
const client = new MisoClient({ env, key, server, debug });
|
|
16
18
|
if (taskId) {
|
|
17
19
|
runOne(client, type, taskId);
|
|
18
20
|
} else {
|
|
@@ -30,14 +32,14 @@ async function runOne(client, type, taskId) {
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
async function runStream(client, type) {
|
|
33
|
-
await
|
|
35
|
+
await pipeline(
|
|
34
36
|
process.stdin,
|
|
35
37
|
split2(),
|
|
36
38
|
client.api[type].statusStream(),
|
|
37
39
|
new stream.OutputStream({
|
|
38
40
|
objectMode: true,
|
|
39
41
|
}),
|
|
40
|
-
|
|
42
|
+
);
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
export default function(type) {
|
package/cli/transform.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { pipeline } from 'stream/promises';
|
|
1
2
|
import split2 from 'split2';
|
|
2
3
|
import { stream } from '@miso.ai/server-commons';
|
|
3
4
|
|
|
@@ -11,19 +12,15 @@ function build(yargs) {
|
|
|
11
12
|
|
|
12
13
|
async function run({ file }) {
|
|
13
14
|
const transform = await stream.getTransformStream(file);
|
|
14
|
-
const streams = [
|
|
15
|
-
process.stdin,
|
|
16
|
-
split2(),
|
|
17
|
-
];
|
|
18
|
-
if (transform.writableObjectMode) {
|
|
19
|
-
streams.push(stream.parse());
|
|
20
|
-
}
|
|
21
|
-
streams.push(transform);
|
|
22
|
-
streams.push(new stream.OutputStream({
|
|
23
|
-
objectMode: transform.readableObjectMode,
|
|
24
|
-
}));
|
|
25
15
|
|
|
26
|
-
await
|
|
16
|
+
await pipeline(
|
|
17
|
+
process.stdin,
|
|
18
|
+
transform.writableObjectMode ? split2(JSON.parse) : split2(),
|
|
19
|
+
transform,
|
|
20
|
+
new stream.OutputStream({
|
|
21
|
+
objectMode: transform.readableObjectMode,
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
27
24
|
}
|
|
28
25
|
|
|
29
26
|
export default {
|
package/cli/upgrade.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { pipeline } from 'stream/promises';
|
|
2
|
+
import split2 from 'split2';
|
|
3
|
+
import { stream, UpgradeChannel, splitObj } from '@miso.ai/server-commons';
|
|
4
|
+
|
|
5
|
+
function build(yargs) {
|
|
6
|
+
return yargs
|
|
7
|
+
.option('name', {
|
|
8
|
+
describe: 'Channel name',
|
|
9
|
+
type: 'string',
|
|
10
|
+
})
|
|
11
|
+
.option('id-field', {
|
|
12
|
+
alias: 'id',
|
|
13
|
+
describe: 'Payload field to use as ID',
|
|
14
|
+
type: 'string',
|
|
15
|
+
})
|
|
16
|
+
.option('as-id', {
|
|
17
|
+
describe: 'Upgrade as ID',
|
|
18
|
+
type: 'boolean',
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function run(options) {
|
|
23
|
+
const [upgradeOptions] = splitObj(options, ['name', 'asId', 'idField']);
|
|
24
|
+
await pipeline(
|
|
25
|
+
process.stdin,
|
|
26
|
+
split2(),
|
|
27
|
+
new UpgradeChannel({ objectMode: false, ...upgradeOptions }),
|
|
28
|
+
new stream.OutputStream({ objectMode: true }),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default {
|
|
33
|
+
command: ['upgrade', 'up'],
|
|
34
|
+
description: `Upgrade the stream to a channel`,
|
|
35
|
+
builder: build,
|
|
36
|
+
handler: run,
|
|
37
|
+
};
|
package/cli/upload.js
CHANGED
|
@@ -1,33 +1,20 @@
|
|
|
1
|
+
import { pipeline } from 'stream/promises';
|
|
1
2
|
import split2 from 'split2';
|
|
2
|
-
import { log, stream } from '@miso.ai/server-commons';
|
|
3
|
+
import { log, stream, splitObj } from '@miso.ai/server-commons';
|
|
3
4
|
import { MisoClient, logger, normalize } from '../src/index.js';
|
|
5
|
+
import { buildForWrite } from './utils.js';
|
|
4
6
|
|
|
5
7
|
function build(yargs) {
|
|
6
|
-
return yargs
|
|
8
|
+
return buildForWrite(yargs)
|
|
7
9
|
.option('dry-run', {
|
|
8
10
|
alias: ['dry'],
|
|
9
11
|
describe: 'Dry run mode',
|
|
12
|
+
type: 'boolean',
|
|
10
13
|
})
|
|
11
14
|
.option('lenient', {
|
|
12
15
|
describe: 'Accept some lenient record schema',
|
|
13
16
|
type: 'boolean',
|
|
14
17
|
})
|
|
15
|
-
.option('requests-per-second', {
|
|
16
|
-
alias: ['rps'],
|
|
17
|
-
describe: 'How many requests to send per second',
|
|
18
|
-
})
|
|
19
|
-
.option('records-per-request', {
|
|
20
|
-
alias: ['rpr'],
|
|
21
|
-
describe: 'How many records to send in a request',
|
|
22
|
-
})
|
|
23
|
-
.option('bytes-per-request', {
|
|
24
|
-
alias: ['bpr'],
|
|
25
|
-
describe: 'How many bytes to send in a request',
|
|
26
|
-
})
|
|
27
|
-
.option('bytes-per-second', {
|
|
28
|
-
alias: ['bps'],
|
|
29
|
-
describe: 'How many bytes to send per second',
|
|
30
|
-
})
|
|
31
18
|
.option('progress', {
|
|
32
19
|
alias: ['p'],
|
|
33
20
|
describe: 'Set log format progress',
|
|
@@ -46,8 +33,35 @@ function build(yargs) {
|
|
|
46
33
|
}
|
|
47
34
|
|
|
48
35
|
const run = type => async ({
|
|
36
|
+
env,
|
|
49
37
|
key,
|
|
50
38
|
server,
|
|
39
|
+
channel,
|
|
40
|
+
...options
|
|
41
|
+
}) => {
|
|
42
|
+
const { debug } = options;
|
|
43
|
+
const client = new MisoClient({ env, key, server, debug });
|
|
44
|
+
|
|
45
|
+
if (channel) {
|
|
46
|
+
await runChannel(client, type, options);
|
|
47
|
+
} else {
|
|
48
|
+
await runStream(client, type, options);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
async function runChannel(client, type, options) {
|
|
53
|
+
const [uploadOptions] = splitObj(options, ['dryRun', 'params', 'requestsPerSecond', 'bytesPerSecond', 'recordsPerRequest', 'bytesPerRequest', 'debug']);
|
|
54
|
+
const uploadChannel = client.api[type].uploadChannel(uploadOptions);
|
|
55
|
+
|
|
56
|
+
await pipeline(
|
|
57
|
+
process.stdin,
|
|
58
|
+
split2(JSON.parse),
|
|
59
|
+
uploadChannel,
|
|
60
|
+
new stream.OutputStream({ objectMode: true }),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function runStream(client, type, {
|
|
51
65
|
param: params,
|
|
52
66
|
['dry-run']: dryRun,
|
|
53
67
|
lenient,
|
|
@@ -55,19 +69,16 @@ const run = type => async ({
|
|
|
55
69
|
['records-per-request']: recordsPerRequest,
|
|
56
70
|
['bytes-per-request']: bytesPerRequest,
|
|
57
71
|
['bytes-per-second']: bytesPerSecond,
|
|
58
|
-
['experiment-id']: experimentId,
|
|
59
|
-
debug,
|
|
60
72
|
progress,
|
|
73
|
+
debug,
|
|
74
|
+
['experiment-id']: experimentId,
|
|
61
75
|
['stream-name']: name,
|
|
62
76
|
['log-level']: loglevel,
|
|
63
77
|
['log-format']: logFormat,
|
|
64
|
-
})
|
|
65
|
-
|
|
78
|
+
}) {
|
|
66
79
|
loglevel = (debug || progress) ? log.DEBUG : loglevel;
|
|
67
80
|
logFormat = progress ? logger.FORMAT.PROGRESS : logFormat;
|
|
68
81
|
|
|
69
|
-
const client = new MisoClient({ key, server, debug });
|
|
70
|
-
|
|
71
82
|
const uploadStreamObjectMode = lenient;
|
|
72
83
|
|
|
73
84
|
const uploadStream = client.api[type].uploadStream({
|
|
@@ -97,17 +108,18 @@ const run = type => async ({
|
|
|
97
108
|
// lenient: stdin -> split2 -> parse -> normalize -> upload -> log
|
|
98
109
|
// notice that the output of split2 are strings, while input/output of normalize are objects
|
|
99
110
|
|
|
100
|
-
await
|
|
111
|
+
await pipeline(
|
|
101
112
|
process.stdin,
|
|
102
|
-
split2(),
|
|
103
113
|
...(lenient ? [
|
|
104
|
-
|
|
114
|
+
split2(JSON.parse),
|
|
105
115
|
new normalize.Stream(type),
|
|
106
|
-
] : [
|
|
116
|
+
] : [
|
|
117
|
+
split2(),
|
|
118
|
+
]),
|
|
107
119
|
uploadStream,
|
|
108
120
|
logStream,
|
|
109
121
|
);
|
|
110
|
-
}
|
|
122
|
+
}
|
|
111
123
|
|
|
112
124
|
export default function(type) {
|
|
113
125
|
return {
|
package/cli/utils.js
CHANGED
|
@@ -6,6 +6,10 @@ export function buildForApi(yargs) {
|
|
|
6
6
|
alias: ['k', 'api-key'],
|
|
7
7
|
describe: 'API key',
|
|
8
8
|
})
|
|
9
|
+
.option('env', {
|
|
10
|
+
describe: 'Environment',
|
|
11
|
+
type: 'string',
|
|
12
|
+
})
|
|
9
13
|
.option('server', {
|
|
10
14
|
alias: ['api-server'],
|
|
11
15
|
describe: 'API server',
|
|
@@ -13,14 +17,13 @@ export function buildForApi(yargs) {
|
|
|
13
17
|
.option('param', {
|
|
14
18
|
alias: ['v', 'var'],
|
|
15
19
|
describe: 'Extra URL parameters',
|
|
16
|
-
type: '
|
|
20
|
+
type: 'string',
|
|
17
21
|
coerce: _yargs.coerceToArray,
|
|
18
22
|
})
|
|
19
23
|
.option('debug', {
|
|
20
24
|
describe: 'Set log level to debug',
|
|
21
25
|
type: 'boolean',
|
|
22
|
-
})
|
|
23
|
-
.demandOption(['key'], 'API key is required.');
|
|
26
|
+
});
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export function buildForSearch(yargs) {
|
|
@@ -46,6 +49,36 @@ export function buildForSearch(yargs) {
|
|
|
46
49
|
});
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
export function buildForWrite(yargs) {
|
|
53
|
+
return yargs
|
|
54
|
+
.option('channel', {
|
|
55
|
+
alias: ['c'],
|
|
56
|
+
describe: 'Use channel protocol',
|
|
57
|
+
type: 'boolean',
|
|
58
|
+
default: false,
|
|
59
|
+
})
|
|
60
|
+
.option('requests-per-second', {
|
|
61
|
+
alias: ['rps'],
|
|
62
|
+
describe: 'How many requests to send per second',
|
|
63
|
+
type: 'number',
|
|
64
|
+
})
|
|
65
|
+
.option('bytes-per-second', {
|
|
66
|
+
alias: ['bps'],
|
|
67
|
+
describe: 'How many bytes to send per second',
|
|
68
|
+
type: 'number',
|
|
69
|
+
})
|
|
70
|
+
.option('records-per-request', {
|
|
71
|
+
alias: ['rpr'],
|
|
72
|
+
describe: 'How many records to send in a request',
|
|
73
|
+
type: 'number',
|
|
74
|
+
})
|
|
75
|
+
.option('bytes-per-request', {
|
|
76
|
+
alias: ['bpr'],
|
|
77
|
+
describe: 'How many bytes to send in a request',
|
|
78
|
+
type: 'number',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
49
82
|
export function formatError(err) {
|
|
50
83
|
const { response } = err;
|
|
51
84
|
if (response) {
|
package/package.json
CHANGED
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
"simonpai <simon.pai@askmiso.com>"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@miso.ai/server-commons": "0.6.6
|
|
19
|
+
"@miso.ai/server-commons": "0.6.6",
|
|
20
20
|
"axios": "^1.6.2",
|
|
21
21
|
"axios-retry": "^4.5.0",
|
|
22
22
|
"dotenv": "^16.0.1",
|
|
23
23
|
"split2": "^4.1.0",
|
|
24
24
|
"yargs": "^17.5.1"
|
|
25
25
|
},
|
|
26
|
-
"version": "0.6.6
|
|
26
|
+
"version": "0.6.6"
|
|
27
27
|
}
|
package/src/api/base.js
CHANGED
|
@@ -4,6 +4,7 @@ import UploadStream from '../stream/upload.js';
|
|
|
4
4
|
import DeleteStream from '../stream/delete.js';
|
|
5
5
|
import StatusStream from '../stream/status.js';
|
|
6
6
|
import MergeStream from '../stream/merge.js';
|
|
7
|
+
import { UploadChannel, DeleteChannel } from '../channel/index.js';
|
|
7
8
|
|
|
8
9
|
export class Queries {
|
|
9
10
|
|
|
@@ -35,6 +36,10 @@ export class Writable {
|
|
|
35
36
|
return new UploadStream(this._client, this._type, options);
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
uploadChannel(options = {}) {
|
|
40
|
+
return new UploadChannel(this._client, this._type, options);
|
|
41
|
+
}
|
|
42
|
+
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
export class Entities extends Writable {
|
|
@@ -44,7 +49,7 @@ export class Entities extends Writable {
|
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
async get(id) {
|
|
47
|
-
const url = buildUrl(this._client, `${this._type}/${id}`);
|
|
52
|
+
const url = buildUrl(this._client, `${this._type}/${encodeURIComponent(id)}`);
|
|
48
53
|
return (await this._client._axios.get(url)).data.data;
|
|
49
54
|
}
|
|
50
55
|
|
|
@@ -62,6 +67,10 @@ export class Entities extends Writable {
|
|
|
62
67
|
return new DeleteStream(this._client, this._type, options);
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
deleteChannel(options = {}) {
|
|
71
|
+
return new DeleteChannel(this._client, this._type, options);
|
|
72
|
+
}
|
|
73
|
+
|
|
65
74
|
async status(taskId) {
|
|
66
75
|
const url = buildUrl(this._client, `${this._type}/_status/${taskId}`);
|
|
67
76
|
return (await this._client._axios.get(url)).data;
|
package/src/api/helpers.js
CHANGED
|
@@ -14,12 +14,15 @@ export async function upload(client, type, records, options = {}) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
async function recoverValidRecords(client, type, records, options, response) {
|
|
17
|
-
if (!response || response.status !== 422
|
|
17
|
+
if (!response || response.status !== 422) {
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
20
|
records = extractRecordsFromUploadPayload(records);
|
|
21
21
|
// try to collect valid records and resend them, which should pass the validation
|
|
22
|
-
const { groups = [], unrecognized = [] } = process422ResponseBody(records, response.data); // it takes records too
|
|
22
|
+
const { groups = [], unrecognized = [] } = response.issues = process422ResponseBody(records, response.data); // it takes records too
|
|
23
|
+
if (!options.recoverValidRecordsOn422) {
|
|
24
|
+
return; // still write issues to response
|
|
25
|
+
}
|
|
23
26
|
if (groups.length === 0 || groups.length === records.length || unrecognized.length > 0) {
|
|
24
27
|
// if there are unrecognized messages, it's hard to tell which records are valid
|
|
25
28
|
return;
|
|
@@ -31,15 +34,18 @@ async function recoverValidRecords(client, type, records, options, response) {
|
|
|
31
34
|
}
|
|
32
35
|
const url = buildUrl(client, type, options);
|
|
33
36
|
const validPayload = buildUploadPayload(validRecords);
|
|
37
|
+
let secondResponse;
|
|
34
38
|
try {
|
|
35
|
-
await client._axios.post(url, validPayload);
|
|
39
|
+
secondResponse = await client._axios.post(url, validPayload);
|
|
36
40
|
} catch (_) {
|
|
37
41
|
return; // still fail, never mind...
|
|
38
42
|
}
|
|
39
|
-
response.recovered = {
|
|
43
|
+
response.recovered = trimObj({
|
|
44
|
+
...secondResponse,
|
|
45
|
+
product_ids: validRecords.map(record => record.product_id),
|
|
40
46
|
records: validRecords.length,
|
|
41
47
|
bytes: validPayload.length * 2,
|
|
42
|
-
};
|
|
48
|
+
});
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
export async function merge(client, type, record, { mergeFn = defaultMerge } = {}) {
|
|
@@ -131,18 +137,15 @@ export function process422ResponseBody(payload, { data } = {}) {
|
|
|
131
137
|
}
|
|
132
138
|
|
|
133
139
|
export async function batchDelete(client, type, ids, options = {}) {
|
|
134
|
-
const url = buildUrl(client, `${type}/_delete`,
|
|
140
|
+
const url = buildUrl(client, `${type}/_delete`, options);
|
|
135
141
|
const payload = buildBatchDeletePayload(type, ids);
|
|
136
142
|
const { data } = await client._axios.post(url, payload);
|
|
137
143
|
return data;
|
|
138
144
|
}
|
|
139
145
|
|
|
140
|
-
export function buildUrl(client, path, {
|
|
146
|
+
export function buildUrl(client, path, { dryRun, params: extraParams } = {}) {
|
|
141
147
|
let { server, key } = client._options;
|
|
142
148
|
let params = `?api_key=${key}`;
|
|
143
|
-
if (async) {
|
|
144
|
-
params += '&async=1';
|
|
145
|
-
}
|
|
146
149
|
if (dryRun) {
|
|
147
150
|
params += '&dry_run=1';
|
|
148
151
|
}
|
package/src/api/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import Products from './products.js';
|
|
2
2
|
import Users from './users.js';
|
|
3
3
|
import Interactions from './interactions.js';
|
|
4
|
-
import Experiments from './experiments.js';
|
|
5
4
|
import Ask from './ask.js';
|
|
6
5
|
import Search from './search.js';
|
|
7
6
|
import Recommendation from './recommendation.js';
|
|
@@ -12,7 +11,6 @@ export default class Api {
|
|
|
12
11
|
this.products = new Products(client);
|
|
13
12
|
this.users = new Users(client);
|
|
14
13
|
this.interactions = new Interactions(client);
|
|
15
|
-
this.experiments = new Experiments(client);
|
|
16
14
|
this.ask = new Ask(client);
|
|
17
15
|
this.search = new Search(client);
|
|
18
16
|
this.recommendation = new Recommendation(client);
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { WriteChannelSink, WriteChannel, trimObj } from '@miso.ai/server-commons';
|
|
2
|
+
|
|
3
|
+
export function normalizeApiSinkGateOptions({
|
|
4
|
+
writesPerSecond = 10,
|
|
5
|
+
recordsPerSecond = 100000,
|
|
6
|
+
bytesPerSecond = 100 * 1024 * 1024,
|
|
7
|
+
...options
|
|
8
|
+
} = {}) {
|
|
9
|
+
return {
|
|
10
|
+
writesPerSecond,
|
|
11
|
+
recordsPerSecond,
|
|
12
|
+
bytesPerSecond,
|
|
13
|
+
...options,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeParams(params) {
|
|
18
|
+
if (!params || params.length === 0) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
return params.reduce((acc, param) => {
|
|
22
|
+
const [key, value = '1'] = param.split('=');
|
|
23
|
+
acc[key] = value;
|
|
24
|
+
return acc;
|
|
25
|
+
}, {});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function splitData(data, ids) {
|
|
29
|
+
ids = new Set(ids);
|
|
30
|
+
const positive = [];
|
|
31
|
+
const negative = [];
|
|
32
|
+
for (const record of data) {
|
|
33
|
+
if (ids.has(record.id)) {
|
|
34
|
+
positive.push(record);
|
|
35
|
+
} else {
|
|
36
|
+
negative.push(record);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return [positive, negative];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeIssuesToData(data, { groups = [] } = {}, { name: channel } = {}) {
|
|
43
|
+
for (const { index, violations = [] } of groups) {
|
|
44
|
+
try {
|
|
45
|
+
const errors = (data[index].errors || (data[index].errors = []));
|
|
46
|
+
errors.push(...(violations.map(v => trimObj({ channel, status: 422, ...v }))));
|
|
47
|
+
} catch(_) {}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function writeResponseErrorToFailedData(response, { name: channel } = {}) {
|
|
52
|
+
// 422 errors are already handled by writeIssuesToData
|
|
53
|
+
if (!response.error && (response.status < 400 || response.status === 422)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const error = trimObj({
|
|
57
|
+
channel,
|
|
58
|
+
status: response.status,
|
|
59
|
+
message: response.error || response.statusText,
|
|
60
|
+
});
|
|
61
|
+
for (const event of response.failed.data) {
|
|
62
|
+
(event.errors || (event.errors = [])).push({ ...error });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class ChannelApiSink extends WriteChannelSink {
|
|
67
|
+
|
|
68
|
+
constructor(client, options) {
|
|
69
|
+
super(options);
|
|
70
|
+
this._client = client;
|
|
71
|
+
// TODO: take axios options?
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_normalizeOptions({
|
|
75
|
+
params,
|
|
76
|
+
dryRun,
|
|
77
|
+
...options
|
|
78
|
+
} = {}) {
|
|
79
|
+
return trimObj({
|
|
80
|
+
...super._normalizeOptions(options),
|
|
81
|
+
params: normalizeParams(params),
|
|
82
|
+
dryRun: !!dryRun,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async _write(request) {
|
|
87
|
+
const { payload, records, data } = request;
|
|
88
|
+
let response;
|
|
89
|
+
try {
|
|
90
|
+
response = await this._send(payload);
|
|
91
|
+
response = processMisoApiResponse(response);
|
|
92
|
+
response.writes = 1;
|
|
93
|
+
response.successful = { records, data };
|
|
94
|
+
response.failed = { records: 0, data: [] };
|
|
95
|
+
} catch(error) {
|
|
96
|
+
// not axios-handled error
|
|
97
|
+
if (!error.response) {
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
response = processMisoApiResponse(error.response);
|
|
101
|
+
if (typeof response !== 'object') {
|
|
102
|
+
response = trimObj({ error: response });
|
|
103
|
+
}
|
|
104
|
+
const { recovered, issues } = error.response;
|
|
105
|
+
// write issues into failed data events
|
|
106
|
+
// TODO: need to add channel name
|
|
107
|
+
writeIssuesToData(data, issues, this._channel);
|
|
108
|
+
// TODO: handle issue.unrecognized
|
|
109
|
+
|
|
110
|
+
if (recovered) {
|
|
111
|
+
const [positive, negative] = splitData(data, recovered.product_ids);
|
|
112
|
+
response.writes = 2;
|
|
113
|
+
response.successful = { records: positive.length, data: positive };
|
|
114
|
+
response.failed = { records: negative.length, data: negative };
|
|
115
|
+
response.recovered = processMisoApiResponse(recovered);
|
|
116
|
+
} else {
|
|
117
|
+
response.writes = 1;
|
|
118
|
+
response.successful = { records: 0, data: [] };
|
|
119
|
+
response.failed = { records, data };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// TODO: need to add channel name
|
|
124
|
+
writeResponseErrorToFailedData(response, this._channel);
|
|
125
|
+
|
|
126
|
+
return response;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async _send(payload) {
|
|
130
|
+
throw new Error(`Unimplemented.`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export class ApiWriteChannel extends WriteChannel {
|
|
136
|
+
|
|
137
|
+
constructor(client, type, options = {}) {
|
|
138
|
+
super({
|
|
139
|
+
...options,
|
|
140
|
+
sinkGate: normalizeApiSinkGateOptions(options),
|
|
141
|
+
});
|
|
142
|
+
this._client = client;
|
|
143
|
+
this._type = type;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async _runCustomTransform(event) {
|
|
147
|
+
switch (event.type) {
|
|
148
|
+
case 'data':
|
|
149
|
+
await this._runData(event);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
await super._runCustomTransform(event);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async _runData(event) {
|
|
156
|
+
await this.writeData(event);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
_createWriteEvent(context) {
|
|
160
|
+
return trimObj({
|
|
161
|
+
...super._createWriteEvent(context),
|
|
162
|
+
taskId: getTaskIdFromResponse(context.response),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getTaskIdFromResponse(response) {
|
|
169
|
+
return getTaskIdFromResponse0(response) || getTaskIdFromResponse0(response.recovered);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function getTaskIdFromResponse0({ body } = {}) {
|
|
173
|
+
return body && body.data && body.data.task_id;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function maskApiKeyInMisoUrl(url) {
|
|
177
|
+
// mask the api_key from the url
|
|
178
|
+
return url.replace(/api_key=\w+/, 'api_key=****');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function processMisoApiResponse(response) {
|
|
182
|
+
if (typeof response !== 'object') {
|
|
183
|
+
return response;
|
|
184
|
+
}
|
|
185
|
+
const { data: body, status, statusText, config = {} } = response;
|
|
186
|
+
const { method, url } = config;
|
|
187
|
+
return trimObj({
|
|
188
|
+
status,
|
|
189
|
+
statusText,
|
|
190
|
+
method,
|
|
191
|
+
url: maskApiKeyInMisoUrl(url),
|
|
192
|
+
body,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ChannelApiSink, ApiWriteChannel } from './api.js';
|
|
2
|
+
import { batchDelete } from '../api/helpers.js';
|
|
3
|
+
|
|
4
|
+
// channel //
|
|
5
|
+
export default class DeleteChannel extends ApiWriteChannel {
|
|
6
|
+
|
|
7
|
+
constructor(client, type, { name = 'delete', ...options } = {}) {
|
|
8
|
+
super(client, type, {
|
|
9
|
+
...options,
|
|
10
|
+
name,
|
|
11
|
+
buffer: normalizeBufferOptions(type, options),
|
|
12
|
+
sink: createSink(client, type, options),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// buffer //
|
|
19
|
+
const DEFAULT_BUFFER_OPTIONS = Object.freeze({
|
|
20
|
+
payloadSuffix: ']}}',
|
|
21
|
+
payloadDelimiter: ',',
|
|
22
|
+
serialize: event => JSON.stringify(event.id),
|
|
23
|
+
recordCap: 1000,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function normalizeBufferOptions(type, {
|
|
27
|
+
bytesPerRequest,
|
|
28
|
+
recordsPerRequest,
|
|
29
|
+
...options
|
|
30
|
+
}) {
|
|
31
|
+
const key = type === 'products' ? 'product_ids' : 'user_ids';
|
|
32
|
+
options = {
|
|
33
|
+
...DEFAULT_BUFFER_OPTIONS,
|
|
34
|
+
...options,
|
|
35
|
+
payloadPrefix: `{"data":{"${key}":[`,
|
|
36
|
+
};
|
|
37
|
+
if (recordsPerRequest) {
|
|
38
|
+
options.recordCap = recordsPerRequest;
|
|
39
|
+
}
|
|
40
|
+
if (bytesPerRequest) {
|
|
41
|
+
options.byteCap = bytesPerRequest;
|
|
42
|
+
}
|
|
43
|
+
return options;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// sink //
|
|
47
|
+
function createSink(client, type, options) {
|
|
48
|
+
switch (type) {
|
|
49
|
+
case 'users':
|
|
50
|
+
case 'products':
|
|
51
|
+
return new ChannelDeleteSink(client, { ...options, type });
|
|
52
|
+
default:
|
|
53
|
+
throw new Error(`Unrecognized type: ${type}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class ChannelDeleteSink extends ChannelApiSink {
|
|
58
|
+
|
|
59
|
+
constructor(client, options) {
|
|
60
|
+
super(client, options);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async _send(payload) {
|
|
64
|
+
const { type, params } = this._options;
|
|
65
|
+
return await batchDelete(this._client, type, payload, { params });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ChannelApiSink, ApiWriteChannel } from './api.js';
|
|
2
|
+
import { upload } from '../api/helpers.js';
|
|
3
|
+
|
|
4
|
+
// channel //
|
|
5
|
+
export default class UploadChannel extends ApiWriteChannel {
|
|
6
|
+
|
|
7
|
+
constructor(client, type, { name = 'upload', ...options } = {}) {
|
|
8
|
+
super(client, type, {
|
|
9
|
+
...options,
|
|
10
|
+
name,
|
|
11
|
+
buffer: normalizeBufferOptions(type, options),
|
|
12
|
+
sink: createSink(client, type, options),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// buffer //
|
|
19
|
+
const DEFAULT_BUFFER_OPTIONS = Object.freeze({
|
|
20
|
+
payloadPrefix: '{"data":[',
|
|
21
|
+
payloadSuffix: ']}',
|
|
22
|
+
payloadDelimiter: ',',
|
|
23
|
+
byteCap: 1024 * 1024,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function normalizeBufferOptions(type, { recordsPerRequest, bytesPerRequest, ...options } = {}) {
|
|
27
|
+
options = { ...DEFAULT_BUFFER_OPTIONS, ...options };
|
|
28
|
+
switch (type) {
|
|
29
|
+
case 'users':
|
|
30
|
+
case 'products':
|
|
31
|
+
options.recordCap = recordsPerRequest || 200;
|
|
32
|
+
break;
|
|
33
|
+
case 'interactions':
|
|
34
|
+
options.recordCap = recordsPerRequest || 1000;
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
throw new Error(`Unrecognized type: ${type}`);
|
|
38
|
+
}
|
|
39
|
+
if (bytesPerRequest) {
|
|
40
|
+
options.byteCap = bytesPerRequest;
|
|
41
|
+
}
|
|
42
|
+
return options;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// sink //
|
|
46
|
+
function createSink(client, type, options) {
|
|
47
|
+
switch (type) {
|
|
48
|
+
case 'users':
|
|
49
|
+
case 'products':
|
|
50
|
+
case 'interactions':
|
|
51
|
+
return new ChannelUploadSink(client, { ...options, type });
|
|
52
|
+
default:
|
|
53
|
+
throw new Error(`Unrecognized type: ${type}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class ChannelUploadSink extends ChannelApiSink {
|
|
58
|
+
|
|
59
|
+
constructor(client, options) {
|
|
60
|
+
super(client, options);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async _send(payload) {
|
|
64
|
+
const { type, dryRun, params } = this._options;
|
|
65
|
+
return await upload(this._client, type, payload, { recoverValidRecordsOn422: true, dryRun, params });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
}
|
package/src/client.js
CHANGED
|
@@ -28,6 +28,7 @@ function normalizeOptions(options) {
|
|
|
28
28
|
if (typeof options === 'string') {
|
|
29
29
|
options = { key: options };
|
|
30
30
|
}
|
|
31
|
+
options.key = getApiKeyFromEnv(options.env, options.key);
|
|
31
32
|
if (!options.key || typeof options.key !== 'string') {
|
|
32
33
|
throw new Error(`API key is required.`);
|
|
33
34
|
}
|
|
@@ -35,3 +36,10 @@ function normalizeOptions(options) {
|
|
|
35
36
|
|
|
36
37
|
return options;
|
|
37
38
|
}
|
|
39
|
+
|
|
40
|
+
function getApiKeyFromEnv(env, key) {
|
|
41
|
+
if (!env) {
|
|
42
|
+
return key;
|
|
43
|
+
}
|
|
44
|
+
return process.env[`MISO_${env.toUpperCase()}_API_KEY`] || undefined;
|
|
45
|
+
}
|
package/src/stream/api-sink.js
CHANGED
|
@@ -62,10 +62,12 @@ export default class ApiSink extends sink.BpsSink {
|
|
|
62
62
|
if (error.response.recovered) {
|
|
63
63
|
data.recovered = error.response.recovered;
|
|
64
64
|
}
|
|
65
|
+
if (error.response.issues) {
|
|
66
|
+
data.issues = error.response.issues;
|
|
67
|
+
}
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
// keep track of service stats on successful calls
|
|
68
|
-
// TODO: handle recovered records?
|
|
69
71
|
if (!data.errors) {
|
|
70
72
|
this._serviceStats.track({ records, bytes, took: data.took });
|
|
71
73
|
}
|
package/src/stream/delete.js
CHANGED
|
@@ -11,6 +11,8 @@ export default class DeleteStream extends stream.BufferedWriteStream {
|
|
|
11
11
|
objectMode,
|
|
12
12
|
heartbeatInterval,
|
|
13
13
|
// sink
|
|
14
|
+
dryRun,
|
|
15
|
+
params,
|
|
14
16
|
recordsPerSecond,
|
|
15
17
|
// buffer
|
|
16
18
|
recordsPerRequest,
|
|
@@ -35,6 +37,8 @@ export default class DeleteStream extends stream.BufferedWriteStream {
|
|
|
35
37
|
|
|
36
38
|
this._sink = new DeleteSink(client, {
|
|
37
39
|
type,
|
|
40
|
+
dryRun,
|
|
41
|
+
params,
|
|
38
42
|
recordsPerSecond,
|
|
39
43
|
});
|
|
40
44
|
|
|
@@ -28,28 +28,12 @@ class UploadSink extends ApiSink {
|
|
|
28
28
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
class ExperimentEventUploadSink extends UploadSink {
|
|
32
|
-
|
|
33
|
-
constructor(client, options) {
|
|
34
|
-
super(client, { ...options, type: 'experiment-events' });
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async _execute(payload) {
|
|
38
|
-
const { experimentId } = this._options;
|
|
39
|
-
const { data } = await this._client.api.experiments.uploadEvent(experimentId, payload);
|
|
40
|
-
return data;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
31
|
export default function create(client, type, options) {
|
|
46
32
|
switch (type) {
|
|
47
33
|
case 'users':
|
|
48
34
|
case 'products':
|
|
49
35
|
case 'interactions':
|
|
50
36
|
return new UploadSink(client, { ...options, type });
|
|
51
|
-
case 'experiment-events':
|
|
52
|
-
return new ExperimentEventUploadSink(client, options);
|
|
53
37
|
default:
|
|
54
38
|
throw new Error(`Unrecognized type: ${type}`);
|
|
55
39
|
}
|
package/src/stream/upload.js
CHANGED
|
@@ -2,7 +2,6 @@ import { stream } from '@miso.ai/server-commons';
|
|
|
2
2
|
import version from '../version.js';
|
|
3
3
|
import createSink from './upload-sink.js';
|
|
4
4
|
import createBuffer from './upload-buffer.js';
|
|
5
|
-
import { process422ResponseBody } from '../api/helpers.js';
|
|
6
5
|
|
|
7
6
|
export default class UploadStream extends stream.BufferedWriteStream {
|
|
8
7
|
|
|
@@ -67,13 +66,12 @@ export default class UploadStream extends stream.BufferedWriteStream {
|
|
|
67
66
|
|
|
68
67
|
// if upload fails, emit extracted payload at response event
|
|
69
68
|
if (message.event === 'response') {
|
|
70
|
-
// TODO: we can do these near recoverValidRecords()
|
|
71
69
|
const { response, payload } = args;
|
|
72
70
|
if (payload) {
|
|
73
71
|
output.payload = JSON.parse(payload);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
}
|
|
73
|
+
if (response && response.issues) {
|
|
74
|
+
output.issues = response.issues;
|
|
77
75
|
}
|
|
78
76
|
}
|
|
79
77
|
|
package/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export default '0.6.6
|
|
1
|
+
export default '0.6.6';
|
package/src/api/experiments.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { buildUrl } from './helpers.js';
|
|
2
|
-
|
|
3
|
-
export default class Experiments {
|
|
4
|
-
|
|
5
|
-
constructor(client) {
|
|
6
|
-
this._client = client;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
async uploadEvent(experimentId, record) {
|
|
10
|
-
// TODO: support non-string record
|
|
11
|
-
const url = buildUrl(this._client, `experiments/${experimentId}/events`);
|
|
12
|
-
// TODO: make content type header global
|
|
13
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
14
|
-
const response = await this._client._axios.post(url, record, { headers });
|
|
15
|
-
// 200 response body does not have .data layer
|
|
16
|
-
return response.data ? response : { data: response };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
}
|