@foxtware/mineral 0.1.10 → 0.1.12

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.
@@ -4,8 +4,10 @@ const {
4
4
  FetchClient,
5
5
  appendUrlToBase,
6
6
  fetchClientCommonSteps,
7
+ jsonlToObjectArray,
7
8
  pathAsArray,
8
9
  objectDigNodeAtPath,
10
+ sentenceCaseString,
9
11
  } = require('../utils');
10
12
 
11
13
  const handleMutationUserErrors = async (state) => {
@@ -97,6 +99,80 @@ const movePageInfoToMeta = async (state) => {
97
99
  };
98
100
  };
99
101
 
102
+ // https://shopify.dev/docs/api/usage/bulk-operations/queries
103
+ const parseShopifyJsonl = (jsonl) => {
104
+ const objects = jsonlToObjectArray(jsonl);
105
+ const objectsMap = new Map();
106
+ const objectsWithoutStitching = [];
107
+
108
+ for (const object of objects) {
109
+ const {
110
+ id: gid,
111
+ __parentId: parentGid,
112
+ } = object;
113
+
114
+ if (!gid) {
115
+ objectsWithoutStitching.push(object);
116
+ continue;
117
+ }
118
+
119
+ const [objectType] = gid.split('gid://shopify/')[1].split(/[^a-zA-Z0-9]+/);
120
+
121
+ objectsMap.set(gid, {
122
+ ...object,
123
+ selfType: objectType,
124
+ ...parentGid && { parentGid },
125
+ });
126
+ }
127
+
128
+ const objectTypeToProperty = (objectType) => `${ sentenceCaseString(objectType) }s`;
129
+
130
+ for (const [gid, object] of objectsMap) {
131
+ const {
132
+ selfType,
133
+ parentGid,
134
+ } = object;
135
+
136
+ if (!parentGid) {
137
+ continue;
138
+ }
139
+
140
+ const objectProperty = objectTypeToProperty(selfType);
141
+
142
+ let parentObject = objectsMap.get(parentGid);
143
+ if (!parentObject) {
144
+ continue;
145
+ }
146
+
147
+ const nestedParent = gid.split('?')?.[1];
148
+
149
+ if (nestedParent) {
150
+ let [nestedParentType] = nestedParent.split('=');
151
+ nestedParentType = nestedParentType
152
+ .replaceAll('_id', '')
153
+ .split('_')
154
+ .map((word) => word[0].toUpperCase() + word.slice(1))
155
+ .join('');
156
+ nestedParentType = sentenceCaseString(nestedParentType);
157
+ parentObject = parentObject[nestedParentType];
158
+ }
159
+
160
+ if (!parentObject) {
161
+ continue;
162
+ }
163
+
164
+ parentObject[objectProperty] = parentObject[objectProperty] || [];
165
+ parentObject[objectProperty].push(objectsMap.get(gid));
166
+ }
167
+
168
+ const topLevelObjects = Array.from(objectsMap.values()).filter((object) => !object?.parentGid);
169
+
170
+ return [
171
+ ...topLevelObjects,
172
+ ...objectsWithoutStitching,
173
+ ];
174
+ };
175
+
100
176
  const shopifyClient = new FetchClient({
101
177
  pipeline: [
102
178
  resolveCreds,
@@ -113,5 +189,6 @@ const shopifyClient = new FetchClient({
113
189
  });
114
190
 
115
191
  module.exports = {
192
+ parseShopifyJsonl,
116
193
  shopifyClient,
117
194
  };
@@ -0,0 +1,70 @@
1
+ // https://shopify.dev/docs/api/admin-graphql/latest/mutations/bulkoperationrunquery
2
+
3
+ const { credsValidator } = require('../validators');
4
+ const { ArgsWarden } = require('../utils');
5
+ const { shopifyMutationDo } = require('./shopifyMutationDo');
6
+
7
+ const defaultReturnAttrs = `
8
+ id
9
+ type
10
+ status
11
+ objectCount
12
+ url
13
+ errorCode
14
+ `.trim();
15
+
16
+ const argsWarden = new ArgsWarden([
17
+ ['credsPayload', credsValidator],
18
+ ['query'],
19
+ ]);
20
+
21
+ const shopifyBulkOperationRunQuery = async (
22
+ credsPayload,
23
+ query,
24
+ {
25
+ apiVersion,
26
+ returnAttrs = defaultReturnAttrs,
27
+ } = {},
28
+ ) => {
29
+
30
+ const rejectResponse = await argsWarden.responseIfRejectingArgs({
31
+ credsPayload,
32
+ query,
33
+ });
34
+ if (rejectResponse) {
35
+ return rejectResponse;
36
+ }
37
+
38
+ return shopifyMutationDo(
39
+ credsPayload,
40
+ 'bulkOperationRunQuery',
41
+ {
42
+ mutationVariables: {
43
+ query: {
44
+ type: 'String!',
45
+ value: query,
46
+ },
47
+ },
48
+ returnSchema: `bulkOperation { ${ returnAttrs } }`,
49
+ apiVersion,
50
+ },
51
+ );
52
+ };
53
+
54
+ const funcApiConfig = {
55
+ argsWarden,
56
+ };
57
+
58
+ module.exports = {
59
+ shopifyBulkOperationRunQuery,
60
+ funcApiConfig,
61
+ };
62
+
63
+ /*
64
+ curl -X POST "http://localhost:8000/shopifyBulkOperationRunQuery" \
65
+ -H "Content-Type: application/json" \
66
+ -d '{
67
+ "credsPayload": { "credsPath": "shopify.au" },
68
+ "query": "{ products { edges { node { id title handle } } } }"
69
+ }'
70
+ */
@@ -0,0 +1,127 @@
1
+ const { credsValidator } = require('../validators');
2
+ const {
3
+ ArgsWarden,
4
+ customFetch,
5
+ gidToId,
6
+ wait,
7
+ } = require('../utils');
8
+ const { parseShopifyJsonl } = require('./shopify.utils');
9
+ const { shopifyBulkOperationGet } = require('./shopifyBulkOperationGet');
10
+ const { shopifyBulkOperationRunQuery } = require('./shopifyBulkOperationRunQuery');
11
+
12
+ const bulkOpAttrs = `
13
+ id
14
+ status
15
+ type
16
+ objectCount
17
+ url
18
+ errorCode
19
+ `;
20
+
21
+ const argsWarden = new ArgsWarden([
22
+ ['credsPayload', credsValidator],
23
+ ['query'],
24
+ ]);
25
+
26
+ const shopifyBulkQueryDo = async (
27
+ credsPayload,
28
+ query,
29
+ {
30
+ apiVersion,
31
+ waitForResult = true,
32
+ } = {},
33
+ ) => {
34
+
35
+ const rejectResponse = await argsWarden.responseIfRejectingArgs({
36
+ credsPayload,
37
+ query,
38
+ });
39
+ if (rejectResponse) {
40
+ return rejectResponse;
41
+ }
42
+
43
+ const queryRunResponse = await shopifyBulkOperationRunQuery(
44
+ credsPayload,
45
+ query,
46
+ {
47
+ apiVersion,
48
+ returnAttrs: bulkOpAttrs,
49
+ },
50
+ );
51
+
52
+ if (!waitForResult) {
53
+ return queryRunResponse;
54
+ }
55
+
56
+ if (!queryRunResponse.ok) {
57
+ return queryRunResponse;
58
+ }
59
+
60
+ let bulkOperation = queryRunResponse.data.bulkOperation;
61
+ let bulkOperationId = gidToId(bulkOperation.id);
62
+
63
+ while (['CREATED', 'RUNNING'].includes(bulkOperation?.status)) {
64
+
65
+ if (bulkOperation) {
66
+ await wait(5000);
67
+ }
68
+
69
+ const operationResponse = await shopifyBulkOperationGet(
70
+ credsPayload,
71
+ bulkOperationId,
72
+ {
73
+ apiVersion,
74
+ attrs: bulkOpAttrs,
75
+ },
76
+ );
77
+ if (!operationResponse.ok) {
78
+ return operationResponse;
79
+ }
80
+
81
+ bulkOperation = operationResponse.data;
82
+ }
83
+
84
+ if (bulkOperation.status !== 'COMPLETED') {
85
+ return {
86
+ ok: false,
87
+ error: {
88
+ code: 'BULK_OPERATION_FAILED',
89
+ message: `Bulk operation failed with status ${ bulkOperation.status }`,
90
+ details: bulkOperation,
91
+ },
92
+ };
93
+ }
94
+
95
+ const resultsResponse = await customFetch(bulkOperation.url);
96
+ if (!resultsResponse.ok) {
97
+ return resultsResponse;
98
+ }
99
+
100
+ const results = parseShopifyJsonl(resultsResponse.data);
101
+
102
+ return {
103
+ ok: true,
104
+ data: results,
105
+ meta: {
106
+ bulkOperation,
107
+ },
108
+ };
109
+ };
110
+
111
+ const funcApiConfig = {
112
+ argsWarden,
113
+ };
114
+
115
+ module.exports = {
116
+ shopifyBulkQueryDo,
117
+ funcApiConfig,
118
+ };
119
+
120
+ /*
121
+ curl -X POST "http://localhost:8000/shopifyBulkQueryDo" \
122
+ -H "Content-Type: application/json" \
123
+ -d '{
124
+ "credsPayload": { "credsPath": "shopify.au" },
125
+ "query": "{ products { edges { node { id title handle } } } }"
126
+ }'
127
+ */
@@ -18,12 +18,6 @@ const tagsAddBulkMutation = `
18
18
  }
19
19
  `.trim();
20
20
 
21
- const tagsAddBulkUserErrors = (bulkResult) => {
22
- return bulkResult?.data?.tagsAdd?.userErrors
23
- || bulkResult?.tagsAdd?.userErrors
24
- || [];
25
- };
26
-
27
21
  const argsWarden = new ArgsWarden([
28
22
  ['credsPayload', credsValidator],
29
23
  ['gids', Array.isArray],
@@ -35,9 +29,7 @@ const shopifyTagsAddBulk = async (
35
29
  gids,
36
30
  tags,
37
31
  {
38
- apiVersion,
39
- clientIdentifier,
40
- waitForResult = true,
32
+ ...bulkMutationOptions
41
33
  } = {},
42
34
  ) => {
43
35
 
@@ -61,11 +53,7 @@ const shopifyTagsAddBulk = async (
61
53
  })),
62
54
  },
63
55
  },
64
- {
65
- apiVersion,
66
- clientIdentifier,
67
- waitForResult,
68
- },
56
+ bulkMutationOptions,
69
57
  );
70
58
  };
71
59
 
@@ -75,7 +63,6 @@ const funcApiConfig = {
75
63
 
76
64
  module.exports = {
77
65
  shopifyTagsAddBulk,
78
- tagsAddBulkUserErrors,
79
66
  funcApiConfig,
80
67
  };
81
68
 
@@ -84,7 +71,7 @@ curl -X POST "http://localhost:8000/shopifyTagsAddBulk" \
84
71
  -H "Content-Type: application/json" \
85
72
  -d '{
86
73
  "credsPayload": { "credsPath": "shopify.au" },
87
- "gids": ["gid://shopify/Customer/123"],
88
- "tags": ["aug26_migration"]
74
+ "gids": ["gid://shopify/Customer/123", "gid://shopify/Customer/456"],
75
+ "tags": ["martian_sympathisers"]
89
76
  }'
90
77
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxtware/mineral",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "bin": {
5
5
  "mineral": "bin/mineral.js"
6
6
  },