@miso.ai/server-sdk 0.6.4 → 0.6.5-beta.1
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 +95 -0
- package/cli/ids.js +14 -2
- package/cli/merge.js +44 -3
- package/package.json +2 -2
- package/src/api/base.js +3 -2
- package/src/api/cache.js +48 -0
- package/src/api/helpers.js +24 -11
- package/src/api/index.js +0 -3
- package/src/logger/standard.js +1 -0
- package/src/stream/merge.js +28 -8
- package/src/stream/upload.js +5 -5
- package/src/version.js +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
19
|
+
"@miso.ai/server-commons": "0.6.5-beta.1",
|
|
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.
|
|
25
|
+
"version": "0.6.5-beta.1"
|
|
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
|
|
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
|
|
package/src/api/cache.js
ADDED
|
@@ -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
|
+
}
|
package/src/api/helpers.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
package/src/logger/standard.js
CHANGED
package/src/stream/merge.js
CHANGED
|
@@ -1,18 +1,38 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
|
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/stream/upload.js
CHANGED
|
@@ -66,11 +66,11 @@ export default class UploadStream extends stream.BufferedWriteStream {
|
|
|
66
66
|
// if upload fails, emit extracted payload at response event
|
|
67
67
|
if (message.event === 'response') {
|
|
68
68
|
const { response, payload } = args;
|
|
69
|
-
if (
|
|
70
|
-
output.payload = JSON.parse(payload);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
if (payload) {
|
|
70
|
+
output.payload = JSON.parse(payload);
|
|
71
|
+
}
|
|
72
|
+
if (response && response.status === 422) {
|
|
73
|
+
output.issues = process422ResponseBody(payload, response);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
package/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export default '0.6.
|
|
1
|
+
export default '0.6.5-beta.1';
|