@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 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 stream.pipeline(
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));
@@ -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 stream.pipeline(
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 stream.pipeline(
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 stream.pipeline(
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 stream.pipeline(
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 stream.pipeline(
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 stream.pipeline([
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 stream.pipeline(...streams);
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 stream.pipeline(
111
+ await pipeline(
101
112
  process.stdin,
102
- split2(),
103
113
  ...(lenient ? [
104
- stream.parse(),
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: 'array',
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-beta.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-beta.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;
@@ -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 || !options.recoverValidRecordsOn422) {
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`, { ...options, async: true });
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, { async, dryRun, params: extraParams } = {}) {
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,2 @@
1
+ export { default as DeleteChannel } from './delete.js';
2
+ export { default as UploadChannel } from './upload.js';
@@ -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
+ }
@@ -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
  }
@@ -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
  }
@@ -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
- if (response && response.status === 422) {
75
- output.issues = process422ResponseBody(payload, response);
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-beta.6';
1
+ export default '0.6.6';
@@ -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
- }