@miso.ai/server-sdk 0.6.4-beta.0 → 0.6.5-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/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # Miso Node.js SDK
2
+
3
+ ## Setup
4
+
5
+ 1. Install Node.js.
6
+
7
+ 2. Install the package locally:
8
+
9
+ ```bash
10
+ npm i @miso.ai/server-sdk
11
+ ```
12
+
13
+ Or, install the package globally:
14
+
15
+ ```bash
16
+ npm i -g @miso.ai/server-sdk
17
+ ```
18
+
19
+ Or, use `npx` to run the commands, which will guide the package installation.
20
+
21
+ 3. Put the following settings in your `.env` file:
22
+
23
+ ```env
24
+ MISO_API_KEY=your_api_key
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### Help message
30
+
31
+ ```bash
32
+ miso --help
33
+ miso products --help
34
+ ```
35
+
36
+ ### Get
37
+
38
+ Get a product by `product_id`:
39
+
40
+ ```bash
41
+ miso products get [id]
42
+ ```
43
+
44
+ ### Upload
45
+
46
+ Given a JSON lines file of products:
47
+
48
+ ```jsonl
49
+ {"product_id": "1", ...}
50
+ {"product_id": "2", ...}
51
+ ...
52
+ ```
53
+
54
+ Upload products by piping records into the command:
55
+
56
+ ```bash
57
+ cat records.jsonl | miso products upload
58
+ ```
59
+
60
+ You can dry run:
61
+
62
+ ```bash
63
+ cat records.jsonl | head -20 | miso products upload --dry
64
+ ```
65
+
66
+ When uploading a large amount of records, tt's recommended to display the progress status and pipe errors to a log file:
67
+
68
+ ```bash
69
+ cat records.jsonl | miso products upload -p 2> error.log
70
+ ```
71
+
72
+ The error log is a JSON line file consisting request payloads and response bodies:
73
+
74
+ ```jsonl
75
+ {"response": { (Miso API response) }, "payload": { "data": [ ...(records uploaded) ] }}
76
+ ...
77
+ ```
78
+
79
+ You can extract the failed records from the error log to work on them, so you don't need to reprocess the whole data set again.
80
+
81
+ ### Delete
82
+
83
+ Given a file of product IDs:
84
+
85
+ ```txt
86
+ product_id_1
87
+ product_id_2
88
+ ...
89
+ ```
90
+
91
+ Delete products by piping IDs into the command:
92
+
93
+ ```bash
94
+ cat product_ids.txt | miso products delete -p 2> error.log
95
+ ```
package/cli/ids.js CHANGED
@@ -4,18 +4,30 @@ import { MisoClient } from '../src/index.js';
4
4
  import diff from './ids-diff.js';
5
5
 
6
6
  const build = type => yargs => {
7
- return yargs
7
+ yargs = yargs
8
8
  .command(diff(type));
9
+ // only works for products
10
+ if (type === 'products') {
11
+ yargs = yargs
12
+ .option('type', {
13
+ alias: ['t'],
14
+ describe: 'Only include record of given type',
15
+ type: 'string',
16
+ });
17
+ }
18
+ return yargs;
9
19
  };
10
20
 
11
21
  const run = type => async ({
12
22
  key,
13
23
  server,
24
+ type: recordType,
14
25
  }) => {
15
26
  const client = new MisoClient({ key, server });
16
27
  let ids;
17
28
  try {
18
- ids = await client.api[type].ids();
29
+ const options = recordType ? { type: recordType } : {};
30
+ ids = await client.api[type].ids(options);
19
31
  } catch (err) {
20
32
  console.error(err);
21
33
  throw err;
package/cli/merge.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { join } from 'path';
2
+ import { createReadStream } from 'fs';
3
+ import { createGunzip } from 'zlib';
1
4
  import split2 from 'split2';
2
5
  import { stream } from '@miso.ai/server-commons';
3
6
  import { MisoClient } from '../src/index.js';
@@ -7,18 +10,29 @@ function build(yargs) {
7
10
  .option('file', {
8
11
  alias: ['f'],
9
12
  describe: 'File that contains the merge function',
10
- });
13
+ })
14
+ .option('fetch', {
15
+ describe: 'Fetch records from server',
16
+ type: 'boolean',
17
+ default: true,
18
+ })
19
+ .option('base', {
20
+ alias: ['b'],
21
+ describe: 'Base record file',
22
+ })
11
23
  }
12
24
 
13
25
  const run = type => async ({
14
26
  key,
15
27
  server,
16
28
  file,
29
+ base,
17
30
  ...options
18
31
  }) => {
19
- // TODO: load merge function
32
+ const mergeFn = await getMergeFn(file);
33
+ const records = await buildBaseRecords(base);
20
34
  const client = new MisoClient({ key, server });
21
- const mergeStream = client.api[type].mergeStream(options);
35
+ const mergeStream = client.api[type].mergeStream({ ...options, mergeFn, records });
22
36
  const outputStream = new stream.OutputStream({ objectMode: true });
23
37
  await stream.pipeline(
24
38
  process.stdin,
@@ -37,3 +51,30 @@ export default function(type) {
37
51
  handler: run(type),
38
52
  };
39
53
  }
54
+
55
+ async function getMergeFn(file) {
56
+ if (!file || file === 'default') {
57
+ return undefined;
58
+ }
59
+ try {
60
+ return (await import(join(process.env.PWD, file))).default;
61
+ } catch (e) {
62
+ throw new Error(`Failed to load merge function from ${file}: ${e.message}`);
63
+ }
64
+ }
65
+
66
+ async function buildBaseRecords(file) {
67
+ if (!file) {
68
+ return undefined;
69
+ }
70
+ let readStream = createReadStream(file);
71
+ if (file.endsWith('.gz')) {
72
+ readStream = readStream.pipe(createGunzip());
73
+ }
74
+ readStream = readStream.pipe(split2()).pipe(stream.parse());
75
+ const records = [];
76
+ for await (const record of readStream) {
77
+ records.push(record);
78
+ }
79
+ return records;
80
+ }
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.4-beta.0",
19
+ "@miso.ai/server-commons": "0.6.5-beta.0",
20
20
  "axios": "^1.6.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.4-beta.0"
25
+ "version": "0.6.5-beta.0"
26
26
  }
package/src/api/base.js CHANGED
@@ -49,8 +49,9 @@ export class Entities extends Writable {
49
49
  return (await axios.get(url)).data.data;
50
50
  }
51
51
 
52
- async ids() {
53
- const url = buildUrl(this._client, `${this._type}/_ids`);
52
+ async ids({ type } = {}) {
53
+ const options = type ? { params: { type } } : {};
54
+ const url = buildUrl(this._client, `${this._type}/_ids`, options);
54
55
  return (await axios.get(url)).data.data.ids;
55
56
  }
56
57
 
@@ -0,0 +1,48 @@
1
+ import { getIdProperty, shimRecordForMerging } from './helpers.js';
2
+
3
+ export default class RecordCache {
4
+
5
+ constructor(client, type, { records = [], fetch = true, ...options } = {}) {
6
+ this._client = client;
7
+ this._type = type;
8
+ this._idProp = getIdProperty(type);
9
+ this._options = { fetch, ...options };
10
+
11
+ let idProp;
12
+ switch (type) {
13
+ case 'products':
14
+ idProp = 'product_id';
15
+ break;
16
+ case 'users':
17
+ idProp = 'user_id';
18
+ break;
19
+ default:
20
+ throw new Error(`Unsupported type: ${type}`);
21
+ }
22
+
23
+ this._cache = new Map();
24
+
25
+ for (const record of records) {
26
+ this._cache.set(record[idProp], record);
27
+ }
28
+ }
29
+
30
+ async get(id) {
31
+ if (!this._cache.has(id)) {
32
+ this._cache.set(id, this._fetch(id)); // don't await
33
+ }
34
+ return this._cache.get(id);
35
+ }
36
+
37
+ async _fetch(id) {
38
+ if (this._options.fetch === false) {
39
+ return undefined;
40
+ }
41
+ try {
42
+ return shimRecordForMerging(await this._client.api[this._type].get(id));
43
+ } catch (e) {
44
+ return undefined;
45
+ }
46
+ }
47
+
48
+ }
@@ -24,22 +24,36 @@ export async function merge(client, type, record, { mergeFn = defaultMerge } = {
24
24
  if (!id) {
25
25
  throw new Error(`Record missing ${idProp}.`);
26
26
  }
27
- const base = shimRecordForMerging(await client.api[type].get(id));
27
+ let base;
28
+ try {
29
+ base = shimRecordForMerging(await client.api[type].get(id));
30
+ } catch (e) {}
28
31
  return await mergeFn(base, record);
29
32
  }
30
33
 
31
- function defaultMerge(base, patch) {
32
- return {
34
+ export function defaultMerge(base, patch) {
35
+ return trimObj({
33
36
  ...base,
34
37
  ...patch,
35
- custom_attributes: {
36
- ...base.custom_attributes,
37
- ...patch.custom_attributes,
38
- },
39
- };
38
+ custom_attributes: trimObj({
39
+ ...(base && base.custom_attributes),
40
+ ...(patch && patch.custom_attributes),
41
+ }),
42
+ });
43
+ }
44
+
45
+ export function getIdProperty(type) {
46
+ switch (type) {
47
+ case 'products':
48
+ return 'product_id';
49
+ case 'users':
50
+ return 'user_id';
51
+ default:
52
+ throw new Error(`Unsupported type: ${type}`);
53
+ }
40
54
  }
41
55
 
42
- function shimRecordForMerging(record) {
56
+ export function shimRecordForMerging(record) {
43
57
  for (const key in record) {
44
58
  if (key === 'product_group_id_or_product_id' || key.startsWith('category_path_')) {
45
59
  delete record[key];
@@ -104,8 +118,7 @@ export function buildUrl(client, path, { async, dryRun, params: extraParams } =
104
118
  }
105
119
  if (extraParams) {
106
120
  for (const key in extraParams) {
107
- // TODO: deal with encodeURIComponent
108
- params += `&${key}=${extraParams[key]}`;
121
+ params += `&${encodeURIComponent(key)}=${encodeURIComponent(extraParams[key])}`;
109
122
  }
110
123
  }
111
124
  return `${server}/v1/${path}${params}`;
package/src/api/index.js CHANGED
@@ -1,12 +1,9 @@
1
- import { asArray } from '@miso.ai/server-commons';
2
- import { merge } from './helpers.js';
3
1
  import Products from './products.js';
4
2
  import Users from './users.js';
5
3
  import Interactions from './interactions.js';
6
4
  import Experiments from './experiments.js';
7
5
  import Search from './search.js';
8
6
  import Recommendation from './recommendation.js';
9
- import MergeStream from '../stream/merge.js';
10
7
 
11
8
  export default class Api {
12
9
 
@@ -1,18 +1,38 @@
1
- import { stream } from '@miso.ai/server-commons';
2
- import { merge } from '../api/helpers.js';
1
+ import { Transform } from 'stream';
2
+ import { getIdProperty, defaultMerge } from '../api/helpers.js';
3
+ import RecordCache from '../api/cache.js';
3
4
 
4
- export default class MergeStream extends stream.ParallelTransform {
5
+ export default class MergeStream extends Transform {
5
6
 
6
7
  constructor(client, type, {
7
- mergeFn,
8
+ mergeFn = defaultMerge,
9
+ ...options
8
10
  } = {}) {
9
11
  super({
10
- transform: (record) => merge(client, type, record, { mergeFn }),
11
- controls: {
12
- throttle: 100,
13
- },
14
12
  objectMode: true,
15
13
  });
14
+ this._client = client;
15
+ this._type = type;
16
+ this._idProp = getIdProperty(type);
17
+ this._mergeFn = mergeFn;
18
+ this._cache = new RecordCache(client, type, options);
19
+ }
20
+
21
+ async _transform(record, _, next) {
22
+ const id = record[this._idProp];
23
+ if (!id) {
24
+ this._error(new Error(`Record missing ${this._idProp}.`));
25
+ next();
26
+ return;
27
+ }
28
+ const base = await this._cache.get(id);
29
+ try {
30
+ const merged = await this._mergeFn(base, record);
31
+ merged && this.push(merged);
32
+ } catch (error) {
33
+ this._error(error);
34
+ }
35
+ next();
16
36
  }
17
37
 
18
38
  _error(error) {
package/src/version.js CHANGED
@@ -1 +1 @@
1
- export default '0.6.4-beta.0';
1
+ export default '0.6.5-beta.0';