@miso.ai/server-sdk 0.6.2 → 0.6.3-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/delete.js +1 -1
- package/cli/ids-diff.js +1 -1
- package/cli/ids.js +7 -1
- package/cli/index.js +2 -2
- package/cli/transform.js +1 -1
- package/cli/upload.js +2 -8
- package/package.json +2 -2
- package/src/api/helpers.js +56 -6
- package/src/client.js +0 -85
- package/src/logger/api-progress.js +1 -4
- package/src/stream/api-sink.js +4 -1
- package/src/stream/upload-sink.js +4 -53
- package/src/stream/upload.js +9 -4
- package/src/version.js +1 -1
package/cli/delete.js
CHANGED
|
@@ -51,7 +51,7 @@ const run = type => async ({
|
|
|
51
51
|
|
|
52
52
|
const client = new MisoClient({ key, server });
|
|
53
53
|
|
|
54
|
-
const deleteStream = client.
|
|
54
|
+
const deleteStream = client.api[type].deleteStream({
|
|
55
55
|
name,
|
|
56
56
|
params,
|
|
57
57
|
heartbeatInterval: logFormat === logger.FORMAT.PROGRESS ? 250 : false,
|
package/cli/ids-diff.js
CHANGED
|
@@ -34,7 +34,7 @@ const run = type => async ({
|
|
|
34
34
|
output = output || (plus ? 'plus' : minus ? 'minus' : undefined);
|
|
35
35
|
|
|
36
36
|
const client = new MisoClient({ key, server });
|
|
37
|
-
const misoIds = await client.ids(
|
|
37
|
+
const misoIds = await client.api[type].ids();
|
|
38
38
|
|
|
39
39
|
const diffStream = new stream.DiffStream(misoIds, { output });
|
|
40
40
|
const outputStream = new stream.OutputStream({ objectMode: false });
|
package/cli/ids.js
CHANGED
|
@@ -13,7 +13,13 @@ const run = type => async ({
|
|
|
13
13
|
server,
|
|
14
14
|
}) => {
|
|
15
15
|
const client = new MisoClient({ key, server });
|
|
16
|
-
|
|
16
|
+
let ids;
|
|
17
|
+
try {
|
|
18
|
+
ids = await client.api[type].ids();
|
|
19
|
+
} catch (err) {
|
|
20
|
+
console.error(err);
|
|
21
|
+
throw err;
|
|
22
|
+
}
|
|
17
23
|
|
|
18
24
|
const readStream = Readable.from(ids);
|
|
19
25
|
const outputStream = new stream.OutputStream();
|
package/cli/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { yargs } from '@miso.ai/server-commons';
|
|
3
|
+
import { MisoClient } from '../src/index.js';
|
|
3
4
|
import upload from './upload.js';
|
|
4
5
|
import del from './delete.js';
|
|
5
6
|
import ids from './ids.js';
|
|
6
7
|
import transform from './transform.js';
|
|
7
|
-
import version from '../src/version.js';
|
|
8
8
|
|
|
9
9
|
const interactions = {
|
|
10
10
|
command: 'interactions',
|
|
@@ -58,7 +58,7 @@ yargs.build(yargs => {
|
|
|
58
58
|
.command(users)
|
|
59
59
|
.command(experiments)
|
|
60
60
|
.command(transform)
|
|
61
|
-
.version(version);
|
|
61
|
+
.version(MisoClient.version);
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
|
package/cli/transform.js
CHANGED
package/cli/upload.js
CHANGED
|
@@ -4,16 +4,12 @@ import { MisoClient, logger, normalize } from '../src/index.js';
|
|
|
4
4
|
|
|
5
5
|
function build(yargs) {
|
|
6
6
|
return yargs
|
|
7
|
-
.option('async', {
|
|
8
|
-
alias: ['a'],
|
|
9
|
-
describe: 'Asynchrnous mode',
|
|
10
|
-
})
|
|
11
7
|
.option('dry-run', {
|
|
12
8
|
alias: ['dry'],
|
|
13
9
|
describe: 'Dry run mode',
|
|
14
10
|
})
|
|
15
11
|
.option('lenient', {
|
|
16
|
-
describe: 'Accept some
|
|
12
|
+
describe: 'Accept some lenient record schema',
|
|
17
13
|
type: 'boolean',
|
|
18
14
|
})
|
|
19
15
|
.option('records-per-request', {
|
|
@@ -53,7 +49,6 @@ const run = type => async ({
|
|
|
53
49
|
key,
|
|
54
50
|
server,
|
|
55
51
|
param: params,
|
|
56
|
-
async,
|
|
57
52
|
['dry-run']: dryRun,
|
|
58
53
|
lenient,
|
|
59
54
|
['records-per-request']: recordsPerRequest,
|
|
@@ -74,10 +69,9 @@ const run = type => async ({
|
|
|
74
69
|
|
|
75
70
|
const uploadStreamObjectMode = lenient;
|
|
76
71
|
|
|
77
|
-
const uploadStream = client.
|
|
72
|
+
const uploadStream = client.api[type].uploadStream({
|
|
78
73
|
objectMode: uploadStreamObjectMode,
|
|
79
74
|
name,
|
|
80
|
-
async,
|
|
81
75
|
dryRun,
|
|
82
76
|
params,
|
|
83
77
|
heartbeatInterval: logFormat === logger.FORMAT.PROGRESS ? 250 : false,
|
package/package.json
CHANGED
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
"simonpai <simon.pai@askmiso.com>"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@miso.ai/server-commons": "0.6.
|
|
19
|
+
"@miso.ai/server-commons": "0.6.3-beta.0",
|
|
20
20
|
"axios": "^0.27.2",
|
|
21
21
|
"dotenv": "^16.0.1",
|
|
22
22
|
"split2": "^4.1.0",
|
|
23
23
|
"yargs": "^17.5.1"
|
|
24
24
|
},
|
|
25
|
-
"version": "0.6.
|
|
25
|
+
"version": "0.6.3-beta.0"
|
|
26
26
|
}
|
package/src/api/helpers.js
CHANGED
|
@@ -1,15 +1,48 @@
|
|
|
1
|
-
import { asArray } from '@miso.ai/server-commons';
|
|
1
|
+
import { asArray, trimObj, computeIfAbsent } from '@miso.ai/server-commons';
|
|
2
2
|
import axios from 'axios';
|
|
3
3
|
import { Buffer } from 'buffer';
|
|
4
4
|
|
|
5
5
|
export async function upload(client, type, records, options = {}) {
|
|
6
|
-
const url = buildUrl(client, type, options);
|
|
6
|
+
const url = buildUrl(client, type, { ...options, async: true });
|
|
7
7
|
const payload = buildUploadPayload(records);
|
|
8
8
|
return axios.post(url, payload);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
const RE_422_MSG_LINE = /^\s*data\.(\d+)(?:\.(\S+))?\s+is\s+invalid\.\s+(.*)$/;
|
|
12
|
+
|
|
13
|
+
export function process422ResponseBody(payload, { data } = {}) {
|
|
14
|
+
const records = extractUploadPayload(payload);
|
|
15
|
+
const unrecognized = [];
|
|
16
|
+
const groupsMap = new Map();
|
|
17
|
+
for (const line of data) {
|
|
18
|
+
const m = line.match(RE_422_MSG_LINE);
|
|
19
|
+
if (!m) {
|
|
20
|
+
unrecognized.push(line);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
let [_, index, path, message] = m;
|
|
24
|
+
index = Number(index);
|
|
25
|
+
const { violations } = computeIfAbsent(groupsMap, index, index => ({
|
|
26
|
+
index,
|
|
27
|
+
violations: [],
|
|
28
|
+
record: records[index],
|
|
29
|
+
}));
|
|
30
|
+
violations.push({
|
|
31
|
+
path,
|
|
32
|
+
message,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const groups = [...groupsMap.keys()]
|
|
36
|
+
.sort((a, b) => a - b)
|
|
37
|
+
.map(groupsMap.get.bind(groupsMap));
|
|
38
|
+
return trimObj({
|
|
39
|
+
groups,
|
|
40
|
+
unrecognized: unrecognized.length ? unrecognized : undefined,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
11
44
|
export async function batchDelete(client, type, ids, options = {}) {
|
|
12
|
-
const url = buildUrl(client, `${type}/_delete`, options);
|
|
45
|
+
const url = buildUrl(client, `${type}/_delete`, { ...options, async: true });
|
|
13
46
|
const payload = buildBatchDeletePayload(type, ids);
|
|
14
47
|
// TODO: organize axios
|
|
15
48
|
const { data } = await axios.post(url, payload, {
|
|
@@ -23,10 +56,11 @@ export async function batchDelete(client, type, ids, options = {}) {
|
|
|
23
56
|
export function buildUrl(client, path, { async, dryRun, params: extraParams } = {}) {
|
|
24
57
|
let { server, key } = client._options;
|
|
25
58
|
let params = `?api_key=${key}`;
|
|
59
|
+
if (async) {
|
|
60
|
+
params += '&async=1';
|
|
61
|
+
}
|
|
26
62
|
if (dryRun) {
|
|
27
63
|
params += '&dry_run=1';
|
|
28
|
-
} else if (async) {
|
|
29
|
-
params += '&async=1';
|
|
30
64
|
}
|
|
31
65
|
if (extraParams) {
|
|
32
66
|
for (const key in extraParams) {
|
|
@@ -40,7 +74,23 @@ export function buildUrl(client, path, { async, dryRun, params: extraParams } =
|
|
|
40
74
|
export function buildUploadPayload(records) {
|
|
41
75
|
return typeof records === 'string' ? records :
|
|
42
76
|
Buffer.isBuffer(records) ? records.toString() :
|
|
43
|
-
{ data: Array.isArray(records)? records : [records] };
|
|
77
|
+
{ data: Array.isArray(records) ? records : [records] };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function extractUploadPayload(records) {
|
|
81
|
+
if (Buffer.isBuffer(records)) {
|
|
82
|
+
records = records.toString();
|
|
83
|
+
}
|
|
84
|
+
if (typeof records === 'string') {
|
|
85
|
+
records = JSON.parse(records);
|
|
86
|
+
}
|
|
87
|
+
if (records.data) {
|
|
88
|
+
records = records.data;
|
|
89
|
+
}
|
|
90
|
+
if (!Array.isArray(records)) {
|
|
91
|
+
records = [records];
|
|
92
|
+
}
|
|
93
|
+
return records;
|
|
44
94
|
}
|
|
45
95
|
|
|
46
96
|
export function buildBatchDeletePayload(type, ids) {
|
package/src/client.js
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import { asArray } from '@miso.ai/server-commons';
|
|
2
|
-
//import { createHash } from 'crypto';
|
|
3
|
-
import { Buffer } from 'buffer';
|
|
4
|
-
import axios from 'axios';
|
|
5
1
|
import version from './version.js';
|
|
6
|
-
import UploadStream from './stream/upload.js';
|
|
7
|
-
import DeleteStream from './stream/delete.js';
|
|
8
2
|
import Api from './api/index.js';
|
|
9
3
|
|
|
10
4
|
export default class MisoClient {
|
|
@@ -17,62 +11,6 @@ export default class MisoClient {
|
|
|
17
11
|
this.api = new Api(this);
|
|
18
12
|
}
|
|
19
13
|
|
|
20
|
-
async upload(type, records, options) {
|
|
21
|
-
const url = buildUrl(this, type, options);
|
|
22
|
-
const payload = buildPayload(records);
|
|
23
|
-
return await axios.post(url, payload);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// TODO: extract to .experiment() later
|
|
27
|
-
async uploadExperimentEvent(experimentId, record) {
|
|
28
|
-
// TODO: support non-string record
|
|
29
|
-
const url = buildUrl(this, `experiments/${experimentId}/events`);
|
|
30
|
-
// TODO: make content type header global
|
|
31
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
32
|
-
const response = await axios.post(url, record, { headers });
|
|
33
|
-
// 200 response body does not have .data layer
|
|
34
|
-
return response.data ? response : { data: response };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async ids(type) {
|
|
38
|
-
const url = buildUrl(this, `${type}/_ids`);
|
|
39
|
-
return (await axios.get(url)).data.data.ids;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async delete(type, ids) {
|
|
43
|
-
if (type !== 'products' && type !== 'users') {
|
|
44
|
-
throw new Error(`Only products and users are supported: ${type}`);
|
|
45
|
-
}
|
|
46
|
-
ids = asArray(ids);
|
|
47
|
-
if (ids.length === 0) {
|
|
48
|
-
return { data: {} };
|
|
49
|
-
}
|
|
50
|
-
const payload = {
|
|
51
|
-
data: {
|
|
52
|
-
[type === 'products' ? 'product_ids' : 'user_ids']: ids,
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
return this._delete(type, payload);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async _delete(type, payload, options) {
|
|
59
|
-
const url = buildUrl(this, `${type}/_delete`, options);
|
|
60
|
-
const { data } = await axios.post(url, payload, {
|
|
61
|
-
headers: {
|
|
62
|
-
'Content-Type': 'application/json',
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
return { data };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
createUploadStream(type, options) {
|
|
69
|
-
return new UploadStream(this, type, options);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
createDeleteStream(type, options) {
|
|
73
|
-
return new DeleteStream(this, type, options);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
14
|
get options() {
|
|
77
15
|
const { server, key } = this._options;
|
|
78
16
|
return Object.freeze({
|
|
@@ -95,26 +33,3 @@ function normalizeOptions(options) {
|
|
|
95
33
|
|
|
96
34
|
return options;
|
|
97
35
|
}
|
|
98
|
-
|
|
99
|
-
function buildUrl(client, path, { async, dryRun, params: extraParams } = {}) {
|
|
100
|
-
let { server, key } = client._options;
|
|
101
|
-
let params = `?api_key=${key}`;
|
|
102
|
-
if (dryRun) {
|
|
103
|
-
params += '&dry_run=1';
|
|
104
|
-
} else if (async) {
|
|
105
|
-
params += '&async=1';
|
|
106
|
-
}
|
|
107
|
-
if (extraParams) {
|
|
108
|
-
for (const key in extraParams) {
|
|
109
|
-
// TODO: deal with encodeURIComponent
|
|
110
|
-
params += `&${key}=${extraParams[key]}`;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return `${server}/v1/${path}${params}`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function buildPayload(records) {
|
|
117
|
-
return typeof records === 'string' ? records :
|
|
118
|
-
Buffer.isBuffer(records) ? records.toString() :
|
|
119
|
-
{ data: Array.isArray(records)? records : [records] };
|
|
120
|
-
}
|
|
@@ -55,15 +55,12 @@ export default class ApiProgressLogStream extends stream.LogUpdateStream {
|
|
|
55
55
|
|
|
56
56
|
_configProps(config = {}) {
|
|
57
57
|
const { sink = {}, extra = {} } = config;
|
|
58
|
-
const { dryRun,
|
|
58
|
+
const { dryRun, params } = sink;
|
|
59
59
|
const { lenient } = extra;
|
|
60
60
|
const props = [];
|
|
61
61
|
if (dryRun) {
|
|
62
62
|
props.push('dry-run');
|
|
63
63
|
}
|
|
64
|
-
if (async) {
|
|
65
|
-
props.push('async');
|
|
66
|
-
}
|
|
67
64
|
if (lenient) {
|
|
68
65
|
props.push('lenient');
|
|
69
66
|
}
|
package/src/stream/api-sink.js
CHANGED
|
@@ -45,9 +45,12 @@ export default class ApiSink extends sink.BpsSink {
|
|
|
45
45
|
if (!error.response) {
|
|
46
46
|
throw error;
|
|
47
47
|
}
|
|
48
|
+
const status = error.response.status;
|
|
48
49
|
data = error.response.data;
|
|
49
50
|
if (typeof data !== 'object') {
|
|
50
|
-
data = trimObj({ errors: true, cause: data });
|
|
51
|
+
data = trimObj({ status, errors: true, cause: data });
|
|
52
|
+
} else if (status) {
|
|
53
|
+
data = { status, ...data };
|
|
51
54
|
}
|
|
52
55
|
}
|
|
53
56
|
|
|
@@ -8,7 +8,6 @@ class UploadSink extends ApiSink {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
_normalizeOptions({
|
|
11
|
-
async,
|
|
12
11
|
dryRun,
|
|
13
12
|
...options
|
|
14
13
|
} = {}) {
|
|
@@ -17,62 +16,14 @@ class UploadSink extends ApiSink {
|
|
|
17
16
|
}
|
|
18
17
|
return {
|
|
19
18
|
...super._normalizeOptions(options),
|
|
20
|
-
async: !!async,
|
|
21
19
|
dryRun: !!dryRun,
|
|
22
20
|
};
|
|
23
21
|
}
|
|
24
22
|
|
|
25
23
|
async _execute(payload) {
|
|
26
|
-
const { type,
|
|
27
|
-
const
|
|
28
|
-
return
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
class DataSetUploadSink extends UploadSink {
|
|
34
|
-
|
|
35
|
-
constructor(client, options) {
|
|
36
|
-
super(client, options);
|
|
37
|
-
this._stats.api = { count: 0, bytes: 0, took: 0 };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
_normalizeOptions({
|
|
41
|
-
apiBpsRate,
|
|
42
|
-
apiBpsSampleThreshold = 1024 * 1024,
|
|
43
|
-
...options
|
|
44
|
-
} = {}) {
|
|
45
|
-
return {
|
|
46
|
-
...super._normalizeOptions(options),
|
|
47
|
-
apiBpsRate: this._normalizeApiBpsRate(apiBpsRate, options),
|
|
48
|
-
apiBpsSampleThreshold,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
_normalizeApiBpsRate(apiBpsRate, options) {
|
|
53
|
-
if (options.type === 'interactions' || options.async) {
|
|
54
|
-
// in async mode, apiBps is meaningless
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
if (apiBpsRate === undefined || apiBpsRate === null) {
|
|
58
|
-
// default to 1 if absent
|
|
59
|
-
return 1;
|
|
60
|
-
}
|
|
61
|
-
if (apiBpsRate === false || (!isNaN(apiBpsRate) && apiBpsRate > 0)) {
|
|
62
|
-
// legal values
|
|
63
|
-
return apiBpsRate;
|
|
64
|
-
}
|
|
65
|
-
throw new Error(`Illegal apiBpsRate value: ${apiBpsRate}`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
_targetBps() {
|
|
69
|
-
const { bytesPerSecond: configuredBps, apiBpsRate, apiBpsSampleThreshold } = this._options;
|
|
70
|
-
if (apiBpsRate && this._stats.api.bytes < apiBpsSampleThreshold) {
|
|
71
|
-
// use configured BPS until we have enough data from API response
|
|
72
|
-
return configuredBps;
|
|
73
|
-
}
|
|
74
|
-
const apiBps = this._serviceStats.bps;
|
|
75
|
-
return !isNaN(apiBps) ? Math.max(apiBps * apiBpsRate, configuredBps) : configuredBps;
|
|
24
|
+
const { type, dryRun, params } = this._options;
|
|
25
|
+
const { data } = await upload(this._client, type, payload, { dryRun, params });
|
|
26
|
+
return data;
|
|
76
27
|
}
|
|
77
28
|
|
|
78
29
|
}
|
|
@@ -96,7 +47,7 @@ export default function create(client, type, options) {
|
|
|
96
47
|
case 'users':
|
|
97
48
|
case 'products':
|
|
98
49
|
case 'interactions':
|
|
99
|
-
return new
|
|
50
|
+
return new UploadSink(client, { ...options, type });
|
|
100
51
|
case 'experiment-events':
|
|
101
52
|
return new ExperimentEventUploadSink(client, options);
|
|
102
53
|
default:
|
package/src/stream/upload.js
CHANGED
|
@@ -2,6 +2,7 @@ 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';
|
|
5
6
|
|
|
6
7
|
export default class UploadStream extends stream.BufferedWriteStream {
|
|
7
8
|
|
|
@@ -11,7 +12,6 @@ export default class UploadStream extends stream.BufferedWriteStream {
|
|
|
11
12
|
objectMode,
|
|
12
13
|
heartbeatInterval,
|
|
13
14
|
// sink
|
|
14
|
-
async,
|
|
15
15
|
dryRun,
|
|
16
16
|
params,
|
|
17
17
|
experimentId,
|
|
@@ -35,7 +35,6 @@ export default class UploadStream extends stream.BufferedWriteStream {
|
|
|
35
35
|
this._type = type;
|
|
36
36
|
|
|
37
37
|
this._sink = createSink(client, type, {
|
|
38
|
-
async,
|
|
39
38
|
dryRun,
|
|
40
39
|
params,
|
|
41
40
|
experimentId,
|
|
@@ -65,8 +64,14 @@ export default class UploadStream extends stream.BufferedWriteStream {
|
|
|
65
64
|
const output = super._output(message, args);
|
|
66
65
|
|
|
67
66
|
// if upload fails, emit extracted payload at response event
|
|
68
|
-
if (message.event === 'response'
|
|
69
|
-
|
|
67
|
+
if (message.event === 'response') {
|
|
68
|
+
const { response, payload } = args;
|
|
69
|
+
if (response && response.errors && payload) {
|
|
70
|
+
output.payload = JSON.parse(payload); // TODO: call extractUploadPayload
|
|
71
|
+
if (response.status === 422) {
|
|
72
|
+
output.issues = process422ResponseBody(payload, response);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
// add upload stats
|
package/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export default '0.6.
|
|
1
|
+
export default '0.6.3-beta.0';
|