@miso.ai/server-shopify 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/index.js +27 -0
- package/cli/store/index.js +12 -0
- package/cli/store/products.js +63 -0
- package/package.json +24 -0
- package/src/index.js +1 -0
- package/src/store/client.js +14 -0
- package/src/store/core.js +168 -0
- package/src/store/index.js +1 -0
- package/src/store/products.js +19 -0
- package/src/store/urls.js +0 -0
- package/src/version.js +1 -0
package/cli/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { yargs } from '@miso.ai/server-commons';
|
|
3
|
+
import version from '../src/version.js';
|
|
4
|
+
import store from './store/index.js';
|
|
5
|
+
|
|
6
|
+
yargs.build(yargs => {
|
|
7
|
+
yargs
|
|
8
|
+
.env('MISO_SHOPIFY')
|
|
9
|
+
.option('shop', {
|
|
10
|
+
alias: ['s', 'domain'],
|
|
11
|
+
describe: 'the Shopify shop domain',
|
|
12
|
+
})
|
|
13
|
+
.option('token', {
|
|
14
|
+
alias: ['t'],
|
|
15
|
+
describe: 'Access token',
|
|
16
|
+
})
|
|
17
|
+
.option('api-version', {
|
|
18
|
+
describe: 'the Shopify API version',
|
|
19
|
+
})
|
|
20
|
+
.option('debug', {
|
|
21
|
+
type: 'boolean',
|
|
22
|
+
default: false,
|
|
23
|
+
})
|
|
24
|
+
.hide('debug')
|
|
25
|
+
.command(store)
|
|
26
|
+
.version(version);
|
|
27
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { stream } from '@miso.ai/server-commons';
|
|
2
|
+
import { ShopifyStoreAdminClient } from '../../src/index.js';
|
|
3
|
+
|
|
4
|
+
function build(yargs) {
|
|
5
|
+
return yargs
|
|
6
|
+
.option('count', {
|
|
7
|
+
alias: 'c',
|
|
8
|
+
describe: 'Count number of products',
|
|
9
|
+
type: 'boolean',
|
|
10
|
+
})
|
|
11
|
+
.option('limit', {
|
|
12
|
+
alias: 'n',
|
|
13
|
+
describe: 'Limit number of products to fetch',
|
|
14
|
+
type: 'number',
|
|
15
|
+
})
|
|
16
|
+
.option('ids', {
|
|
17
|
+
describe: 'Retrieve products by IDs',
|
|
18
|
+
})
|
|
19
|
+
.array('ids')
|
|
20
|
+
.coerce('ids', value => value.flatMap(v => `${v}`.split(',').map(v => v.trim())))
|
|
21
|
+
.option('explain', {
|
|
22
|
+
describe: 'Explain the query',
|
|
23
|
+
type: 'boolean',
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function run({ explain, count, ...options }) {
|
|
28
|
+
if (explain) {
|
|
29
|
+
await runExplain({ count, ...options });
|
|
30
|
+
} else if (count) {
|
|
31
|
+
await runCount(options);
|
|
32
|
+
} else {
|
|
33
|
+
await runList(options);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function runCount(options) {
|
|
38
|
+
const client = new ShopifyStoreAdminClient(options);
|
|
39
|
+
const count = await client.products.count();
|
|
40
|
+
console.log(count);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function runList(options) {
|
|
44
|
+
const client = new ShopifyStoreAdminClient(options);
|
|
45
|
+
await stream.pipeline(
|
|
46
|
+
client.products.stream(options),
|
|
47
|
+
new stream.OutputStream({
|
|
48
|
+
objectMode: true,
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function runExplain(options) {
|
|
54
|
+
const client = new ShopifyStoreAdminClient(options);
|
|
55
|
+
console.log(await client.products.explain(options));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default {
|
|
59
|
+
command: 'products',
|
|
60
|
+
desc: 'Shopify store products APIs',
|
|
61
|
+
builder: build,
|
|
62
|
+
handler: run,
|
|
63
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@miso.ai/server-shopify",
|
|
3
|
+
"description": "Miso Shopify API data tools",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"miso-shopify": "cli/index.js"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {},
|
|
13
|
+
"repository": "MisoAI/miso-server-js-sdk",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"contributors": [
|
|
16
|
+
"simonpai <simon.pai@askmiso.com>"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@miso.ai/server-commons": "0.6.3-beta.0",
|
|
20
|
+
"axios": "^0.27.2",
|
|
21
|
+
"axios-retry": "^3.3.1"
|
|
22
|
+
},
|
|
23
|
+
"version": "0.6.3-beta.0"
|
|
24
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './store/index.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineValues } from '@miso.ai/server-commons';
|
|
2
|
+
import Core from './core.js';
|
|
3
|
+
import Products from './products.js';
|
|
4
|
+
|
|
5
|
+
export default class ShopifyStoreAdminClient {
|
|
6
|
+
|
|
7
|
+
constructor(options) {
|
|
8
|
+
const core = this._core = new Core(options);
|
|
9
|
+
defineValues(this, {
|
|
10
|
+
products: new Products(core),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { Readable, Transform } from 'stream';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import axiosRetry from 'axios-retry';
|
|
4
|
+
import { defineValues, trimObj } from '@miso.ai/server-commons';
|
|
5
|
+
|
|
6
|
+
// const STREAM_OPTIONS = ['limit'];
|
|
7
|
+
|
|
8
|
+
export default class Core {
|
|
9
|
+
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
const { shop, apiVersion = '2022-10', token, timeout = 5000 } = this._options = options;
|
|
12
|
+
const baseUrl = `https://${shop}.myshopify.com/admin/api/${apiVersion}`;
|
|
13
|
+
defineValues(this, {
|
|
14
|
+
baseUrl,
|
|
15
|
+
});
|
|
16
|
+
this._axios = axios.create({
|
|
17
|
+
baseURL: baseUrl,
|
|
18
|
+
timeout,
|
|
19
|
+
headers: {
|
|
20
|
+
'Content-Type': 'application/json',
|
|
21
|
+
'X-Shopify-Access-Token': token,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
axiosRetry(this._axios, {
|
|
25
|
+
retries: 5,
|
|
26
|
+
retryDelay: count => count * 300,
|
|
27
|
+
});
|
|
28
|
+
this._start = Date.now();
|
|
29
|
+
this.debug = this.debug.bind(this);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async fetch(path, options = {}) {
|
|
33
|
+
const { method = 'GET', body, params } = options;
|
|
34
|
+
const { status, data, headers } = await this._axios.request({
|
|
35
|
+
url: path,
|
|
36
|
+
method,
|
|
37
|
+
params,
|
|
38
|
+
data: body,
|
|
39
|
+
});
|
|
40
|
+
// TODO: handle status
|
|
41
|
+
const {
|
|
42
|
+
'x-shopify-shop-api-call-limit': callLimit,
|
|
43
|
+
link,
|
|
44
|
+
} = headers;
|
|
45
|
+
const pageInfo = parseLink(link);
|
|
46
|
+
//this.debug({ path, params, status, pageInfo, callLimit, link });
|
|
47
|
+
return trimObj({ data, pageInfo, callLimit });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async explain(resource, { count, ids } = {}) {
|
|
51
|
+
const { baseURL, headers = {} } = this._axios.defaults;
|
|
52
|
+
const path = count ? `/${resource}/count.json` : `/${resource}.json`;
|
|
53
|
+
const params = trimObj({
|
|
54
|
+
ids: encodeIds(ids),
|
|
55
|
+
});
|
|
56
|
+
const headersOptions = Object.entries(headers).map(([k, v]) => typeof v === 'string' ? ` -H '${k}: ${v}'` : '').join('');
|
|
57
|
+
const paramsStr = `${new URLSearchParams(params)}`;
|
|
58
|
+
const url = `${baseURL}${path}${paramsStr ? `?${paramsStr}` : ''}`;
|
|
59
|
+
return `curl${headersOptions} '${url}'`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async count(resource) {
|
|
63
|
+
return (await this.fetch(`/${resource}/count.json`)).data.count;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
stream(resource, options) {
|
|
67
|
+
return new ShopifyStoreResourceStream(this, resource, options)
|
|
68
|
+
.pipe(new FlatmapStream());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
debug(v) {
|
|
72
|
+
if (this._options.debug) {
|
|
73
|
+
const elapsed = (Date.now() - this._start) / 1000;
|
|
74
|
+
console.error(`[${elapsed}]`, v);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseLink(link) {
|
|
81
|
+
if (!link) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return link.split(',').reduce((acc, line) => {
|
|
85
|
+
try {
|
|
86
|
+
const [url, rel] = line.trim().split('; rel=');
|
|
87
|
+
if (rel) {
|
|
88
|
+
const pageInfo = new URL(url.slice(1, -1)).searchParams.get('page_info');
|
|
89
|
+
if (pageInfo) {
|
|
90
|
+
acc[rel.slice(1, -1)] = pageInfo;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch(e) {}
|
|
94
|
+
return acc;
|
|
95
|
+
}, {});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function encodeIds(ids) {
|
|
99
|
+
return ids && ids.map(s => s.trim()).join(',');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// TODO: move to server-commons
|
|
103
|
+
class FlatmapStream extends Transform {
|
|
104
|
+
|
|
105
|
+
constructor() {
|
|
106
|
+
super({ objectMode: true });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async _transform(records, _, next) {
|
|
110
|
+
for (const record of records) {
|
|
111
|
+
this.push(record);
|
|
112
|
+
}
|
|
113
|
+
next();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
class ShopifyStoreResourceStream extends Readable {
|
|
119
|
+
|
|
120
|
+
constructor(core, resource, { ids, limit, pageSize = 200 }) {
|
|
121
|
+
super({ objectMode: true });
|
|
122
|
+
this._core = core;
|
|
123
|
+
this._resource = resource;
|
|
124
|
+
this._pageSize = pageSize;
|
|
125
|
+
this._remaining = limit;
|
|
126
|
+
this._params = trimObj({
|
|
127
|
+
ids: encodeIds(ids),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async _read() {
|
|
132
|
+
if (this._done) {
|
|
133
|
+
this.push(null);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const limit = this._remaining !== undefined ? Math.min(this._remaining, this._pageSize) : this._pageSize;
|
|
137
|
+
const params = trimObj({
|
|
138
|
+
...this._params,
|
|
139
|
+
limit,
|
|
140
|
+
page_info: this._pageInfo,
|
|
141
|
+
});
|
|
142
|
+
const { data, pageInfo } = await this._core.fetch(`/${this._resource}.json`, { params });
|
|
143
|
+
if (pageInfo && pageInfo.next) {
|
|
144
|
+
this._pageInfo = pageInfo.next;
|
|
145
|
+
} else {
|
|
146
|
+
this._done = true;
|
|
147
|
+
}
|
|
148
|
+
let { [this._resource]: records } = data;
|
|
149
|
+
const size = records.length;
|
|
150
|
+
if (this._remaining !== undefined) {
|
|
151
|
+
this._remaining -= size;
|
|
152
|
+
if (this._remaining <= 0) {
|
|
153
|
+
this._done = true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (size < limit) {
|
|
157
|
+
this._done = true;
|
|
158
|
+
} else if (size > limit) {
|
|
159
|
+
records = records.slice(0, limit); // just in case
|
|
160
|
+
}
|
|
161
|
+
this.push(size > 0 ? records : null);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_debug(...args) {
|
|
165
|
+
this._core.debug(...args);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ShopifyStoreAdminClient } from './client.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export default class Products {
|
|
2
|
+
|
|
3
|
+
constructor(core) {
|
|
4
|
+
this._core = core;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
async explain(options) {
|
|
8
|
+
return this._core.explain('products', options);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async count() {
|
|
12
|
+
return this._core.count('products');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
stream(options) {
|
|
16
|
+
return this._core.stream('products', options);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
}
|
|
File without changes
|
package/src/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default '0.6.3-beta.0';
|