@miso.ai/server-sdk 0.6.2-beta.0 → 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 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.createDeleteStream(type, {
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(type);
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
- const ids = await client.ids(type);
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
@@ -27,7 +27,7 @@ async function run({ file }) {
27
27
  objectMode: transform.readableObjectMode,
28
28
  }));
29
29
 
30
- await stream.pipeline(streams);
30
+ await stream.pipeline(...streams);
31
31
  }
32
32
 
33
33
  async function getTransformStream(loc) {
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 enient record schema',
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.createUploadStream(type, {
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.2-beta.0",
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.2-beta.0"
25
+ "version": "0.6.3-beta.0"
26
26
  }
@@ -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, async, params } = sink;
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
  }
@@ -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, async, dryRun, params } = this._options;
27
- const response = await upload(this._client, type, payload, { async, dryRun, params });
28
- return response.data;
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 DataSetUploadSink(client, { ...options, type });
50
+ return new UploadSink(client, { ...options, type });
100
51
  case 'experiment-events':
101
52
  return new ExperimentEventUploadSink(client, options);
102
53
  default:
@@ -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' && args.response && args.response.errors && args.payload) {
69
- output.payload = JSON.parse(args.payload);
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.2-beta.0';
1
+ export default '0.6.3-beta.0';