@foxtware/mineral 0.1.18 → 0.1.19
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/AGENTS.md +1 -0
- package/_build_scripts/createNewFunction.js +10 -5
- package/api/peoplevox/peoplevox.utils.js +25 -0
- package/api/shopify/shopifyFulfillmentCreate.js +180 -0
- package/api/shopify/shopifyFulfillmentTrackingInfoUpdate.js +92 -0
- package/api/shopify/shopifyMetafieldDefinitionPropagate.js +67 -16
- package/api/shopify/shopifyOrderFulfill.js +322 -0
- package/api/starshipit/starshipit.utils.js +28 -3
- package/api/starshipit/starshipitOrdersGet.js +96 -0
- package/api/utils.js +84 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -20,6 +20,7 @@ Notes for AI assistants working in this directory. **Read this file at the start
|
|
|
20
20
|
- docs.md in each platform should not describe the details of our implementation, handlers or anything relating to Mineral. It is for describing the platform's API itself and does not change based on what we do with it. It should also be brief.
|
|
21
21
|
- Usually when making a function that gets multiples of a resource, it should be implemented as a Getter, with a paginator and digester. If done in this way, export platformThingGet and platformThingGetter, using the .bind() syntax seen in other Get functions.
|
|
22
22
|
- When making functions that act on single resources, consider using actionSingleOrMultiple to allow a queue of actions.
|
|
23
|
+
- When altering .yml files, preserve whitespace formatting.
|
|
23
24
|
|
|
24
25
|
## API clients
|
|
25
26
|
- Base URLs for platform clients should live in `{platform}.constants.js`, not creds, if they are static for all users of the API.
|
|
@@ -366,11 +366,16 @@ const selectDirInteractive = async (dirs) => {
|
|
|
366
366
|
}
|
|
367
367
|
|
|
368
368
|
const dirIndex = await askQuestion(`Where does your new function live? \n${
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
369
|
+
[
|
|
370
|
+
'[0] api/ (root)',
|
|
371
|
+
...dirs.map((dir, index) => `[${ index + 1 }] ${ dir }`),
|
|
372
|
+
].join('\n')
|
|
372
373
|
}\n`);
|
|
373
374
|
|
|
375
|
+
if (String(dirIndex) === '0') {
|
|
376
|
+
return '';
|
|
377
|
+
}
|
|
378
|
+
|
|
374
379
|
const dir = dirs[dirIndex - 1];
|
|
375
380
|
|
|
376
381
|
if (!dir) {
|
|
@@ -510,8 +515,8 @@ const createNewFunction = async () => {
|
|
|
510
515
|
name: cliArgs.name,
|
|
511
516
|
});
|
|
512
517
|
|
|
513
|
-
if (dir === null || (
|
|
514
|
-
const availableDirs = dirs.length ? dirs.join(', ') : 'api/ (flat)';
|
|
518
|
+
if (dir === null || (dir !== '' && !dirs.includes(dir))) {
|
|
519
|
+
const availableDirs = dirs.length ? `api/ (root), ${ dirs.join(', ') }` : 'api/ (flat)';
|
|
515
520
|
console.error(`Invalid --dir "${ cliArgs.dir }". Available: ${ availableDirs }`);
|
|
516
521
|
process.exitCode = 1;
|
|
517
522
|
return;
|
|
@@ -100,6 +100,30 @@ const stripEnvelope = (state) => {
|
|
|
100
100
|
};
|
|
101
101
|
};
|
|
102
102
|
|
|
103
|
+
// Peoplevox returns HTTP 200 with ResponseId -1 on failure; Detail holds the message.
|
|
104
|
+
const rejectNegativeResponseId = (state) => {
|
|
105
|
+
const { response } = state;
|
|
106
|
+
const { data } = response || {};
|
|
107
|
+
|
|
108
|
+
if (!response?.ok || data == null || typeof data !== 'object') {
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (Number(data.ResponseId) !== -1) {
|
|
113
|
+
return {};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
response: {
|
|
118
|
+
ok: false,
|
|
119
|
+
data,
|
|
120
|
+
error: {
|
|
121
|
+
detail: data.Detail,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
|
|
103
127
|
const tryToParseDetailAsCsv = async (state) => {
|
|
104
128
|
const { response } = state;
|
|
105
129
|
|
|
@@ -179,6 +203,7 @@ const peoplevoxClient = new FetchClient({
|
|
|
179
203
|
useSoapEnvelope,
|
|
180
204
|
'fetch',
|
|
181
205
|
stripEnvelope,
|
|
206
|
+
rejectNegativeResponseId,
|
|
182
207
|
tryToParseDetailAsCsv,
|
|
183
208
|
unwrapSingleDetail,
|
|
184
209
|
hoistDetail,
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// https://shopify.dev/docs/api/admin-graphql/latest/mutations/fulfillmentCreate
|
|
2
|
+
|
|
3
|
+
const { credsValidator } = require('../validators');
|
|
4
|
+
const { actionSingleOrMultiple, everyIfArray, ArgsWarden, valueProvided } = require('../utils');
|
|
5
|
+
const { shopifyMutationDo } = require('../shopify/shopifyMutationDo');
|
|
6
|
+
|
|
7
|
+
const fulfillmentInputValidator = (fulfillmentInput) => {
|
|
8
|
+
const { lineItemsByFulfillmentOrder } = fulfillmentInput || {};
|
|
9
|
+
if (!Array.isArray(lineItemsByFulfillmentOrder) || lineItemsByFulfillmentOrder.length === 0) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return lineItemsByFulfillmentOrder.every((item) => {
|
|
14
|
+
if (!item?.fulfillmentOrderId) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (item.fulfillmentOrderLineItems) {
|
|
19
|
+
return item.fulfillmentOrderLineItems.every((lineItem) => {
|
|
20
|
+
return lineItem?.id && valueProvided(lineItem?.quantity);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return true;
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const argsWarden = new ArgsWarden([
|
|
29
|
+
['credsPayload', credsValidator],
|
|
30
|
+
['fulfillmentInput', (i) => everyIfArray(fulfillmentInputValidator, i)],
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const defaultReturnFulfillmentAttrs = `
|
|
34
|
+
id
|
|
35
|
+
status
|
|
36
|
+
trackingInfo {
|
|
37
|
+
number
|
|
38
|
+
company
|
|
39
|
+
url
|
|
40
|
+
}
|
|
41
|
+
`.trim();
|
|
42
|
+
|
|
43
|
+
const shopifyFulfillmentCreateSingle = async (
|
|
44
|
+
credsPayload,
|
|
45
|
+
fulfillmentInput,
|
|
46
|
+
{
|
|
47
|
+
apiVersion,
|
|
48
|
+
message,
|
|
49
|
+
returnFulfillmentAttrs = defaultReturnFulfillmentAttrs,
|
|
50
|
+
} = {},
|
|
51
|
+
) => {
|
|
52
|
+
|
|
53
|
+
return shopifyMutationDo(
|
|
54
|
+
credsPayload,
|
|
55
|
+
'fulfillmentCreate',
|
|
56
|
+
{
|
|
57
|
+
mutationVariables: {
|
|
58
|
+
fulfillment: {
|
|
59
|
+
type: 'FulfillmentInput!',
|
|
60
|
+
value: fulfillmentInput,
|
|
61
|
+
},
|
|
62
|
+
...(valueProvided(message) && {
|
|
63
|
+
message: {
|
|
64
|
+
type: 'String',
|
|
65
|
+
value: message,
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
},
|
|
69
|
+
returnSchema: `
|
|
70
|
+
fulfillment { ${ returnFulfillmentAttrs } }
|
|
71
|
+
`.trim(),
|
|
72
|
+
apiVersion,
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const shopifyFulfillmentCreate = async (
|
|
78
|
+
credsPayload,
|
|
79
|
+
fulfillmentInput,
|
|
80
|
+
{
|
|
81
|
+
queueRunOptions,
|
|
82
|
+
apiVersion,
|
|
83
|
+
message,
|
|
84
|
+
returnFulfillmentAttrs = defaultReturnFulfillmentAttrs,
|
|
85
|
+
} = {},
|
|
86
|
+
) => {
|
|
87
|
+
|
|
88
|
+
const rejectResponse = await argsWarden.responseIfRejectingArgs({
|
|
89
|
+
credsPayload,
|
|
90
|
+
fulfillmentInput,
|
|
91
|
+
});
|
|
92
|
+
if (rejectResponse) {
|
|
93
|
+
return rejectResponse;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return actionSingleOrMultiple(
|
|
97
|
+
fulfillmentInput,
|
|
98
|
+
shopifyFulfillmentCreateSingle,
|
|
99
|
+
(fulfillmentInputItem) => ({
|
|
100
|
+
args: [
|
|
101
|
+
credsPayload,
|
|
102
|
+
fulfillmentInputItem,
|
|
103
|
+
{ apiVersion, message, returnFulfillmentAttrs },
|
|
104
|
+
],
|
|
105
|
+
}),
|
|
106
|
+
{
|
|
107
|
+
...(queueRunOptions ? { queueRunOptions } : {}),
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const funcApiConfig = {
|
|
113
|
+
argsWarden,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
shopifyFulfillmentCreate,
|
|
118
|
+
funcApiConfig,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/*
|
|
122
|
+
curl -X POST "http://localhost:8000/shopifyFulfillmentCreate" \
|
|
123
|
+
-H "Content-Type: application/json" \
|
|
124
|
+
-d '{
|
|
125
|
+
"credsPayload": { "credsPath": "shopify.au" },
|
|
126
|
+
"fulfillmentInput": {
|
|
127
|
+
"lineItemsByFulfillmentOrder": [
|
|
128
|
+
{
|
|
129
|
+
"fulfillmentOrderId": "gid://shopify/FulfillmentOrder/1234567890"
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
"notifyCustomer": true,
|
|
133
|
+
"trackingInfo": {
|
|
134
|
+
"number": "1234567890",
|
|
135
|
+
"company": "Australia Post",
|
|
136
|
+
"url": "https://auspost.com.au/mypost/track/details/1234567890"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}'
|
|
140
|
+
|
|
141
|
+
curl -X POST "http://localhost:8000/shopifyFulfillmentCreate" \
|
|
142
|
+
-H "Content-Type: application/json" \
|
|
143
|
+
-d '{
|
|
144
|
+
"credsPayload": { "credsPath": "shopify.au" },
|
|
145
|
+
"fulfillmentInput": [
|
|
146
|
+
{
|
|
147
|
+
"lineItemsByFulfillmentOrder": [
|
|
148
|
+
{
|
|
149
|
+
"fulfillmentOrderId": "gid://shopify/FulfillmentOrder/1234567890"
|
|
150
|
+
}
|
|
151
|
+
],
|
|
152
|
+
"notifyCustomer": true,
|
|
153
|
+
"trackingInfo": {
|
|
154
|
+
"number": "1234567890",
|
|
155
|
+
"company": "Australia Post"
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"lineItemsByFulfillmentOrder": [
|
|
160
|
+
{
|
|
161
|
+
"fulfillmentOrderId": "gid://shopify/FulfillmentOrder/0987654321",
|
|
162
|
+
"fulfillmentOrderLineItems": [
|
|
163
|
+
{
|
|
164
|
+
"id": "gid://shopify/FulfillmentOrderLineItem/9876543210",
|
|
165
|
+
"quantity": 1
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
],
|
|
170
|
+
"notifyCustomer": false,
|
|
171
|
+
"originAddress": {
|
|
172
|
+
"countryCode": "AU"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
],
|
|
176
|
+
"options": {
|
|
177
|
+
"message": "Partial fulfillment"
|
|
178
|
+
}
|
|
179
|
+
}'
|
|
180
|
+
*/
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// https://shopify.dev/docs/api/admin-graphql/latest/mutations/fulfillmentTrackingInfoUpdate
|
|
2
|
+
|
|
3
|
+
const { credsValidator } = require('../validators');
|
|
4
|
+
const { ArgsWarden } = require('../utils');
|
|
5
|
+
const { shopifyMutationDo } = require('../shopify/shopifyMutationDo');
|
|
6
|
+
|
|
7
|
+
const defaultReturnSchema = `
|
|
8
|
+
fulfillment {
|
|
9
|
+
id
|
|
10
|
+
status
|
|
11
|
+
trackingInfo {
|
|
12
|
+
company
|
|
13
|
+
number
|
|
14
|
+
url
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const argsWarden = new ArgsWarden([
|
|
20
|
+
['credsPayload', credsValidator],
|
|
21
|
+
['fulfillmentId'],
|
|
22
|
+
['trackingInfoPayload'],
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const shopifyFulfillmentTrackingInfoUpdate = async (
|
|
26
|
+
credsPayload,
|
|
27
|
+
fulfillmentId,
|
|
28
|
+
trackingInfoPayload,
|
|
29
|
+
{
|
|
30
|
+
apiVersion,
|
|
31
|
+
returnSchema = defaultReturnSchema,
|
|
32
|
+
notifyCustomer = false,
|
|
33
|
+
} = {},
|
|
34
|
+
) => {
|
|
35
|
+
|
|
36
|
+
const rejectResponse = await argsWarden.responseIfRejectingArgs({
|
|
37
|
+
credsPayload,
|
|
38
|
+
fulfillmentId,
|
|
39
|
+
trackingInfoPayload,
|
|
40
|
+
});
|
|
41
|
+
if (rejectResponse) {
|
|
42
|
+
return rejectResponse;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return shopifyMutationDo(
|
|
46
|
+
credsPayload,
|
|
47
|
+
'fulfillmentTrackingInfoUpdate',
|
|
48
|
+
{
|
|
49
|
+
mutationVariables: {
|
|
50
|
+
fulfillmentId: {
|
|
51
|
+
type: 'ID!',
|
|
52
|
+
value: `gid://shopify/Fulfillment/${ fulfillmentId }`,
|
|
53
|
+
},
|
|
54
|
+
trackingInfoInput: {
|
|
55
|
+
type: 'FulfillmentTrackingInput!',
|
|
56
|
+
value: trackingInfoPayload,
|
|
57
|
+
},
|
|
58
|
+
...notifyCustomer && {
|
|
59
|
+
notifyCustomer: {
|
|
60
|
+
type: 'Boolean',
|
|
61
|
+
value: notifyCustomer,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
returnSchema,
|
|
66
|
+
apiVersion,
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const funcApiConfig = {
|
|
72
|
+
argsWarden,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
shopifyFulfillmentTrackingInfoUpdate,
|
|
77
|
+
funcApiConfig,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/*
|
|
81
|
+
curl -X POST "http://localhost:8000/shopifyFulfillmentTrackingInfoUpdate" \
|
|
82
|
+
-H "Content-Type: application/json" \
|
|
83
|
+
-d '{
|
|
84
|
+
"credsPayload": { "credsPath": "shopify.au" },
|
|
85
|
+
"fulfillmentId": "104188477512",
|
|
86
|
+
"trackingInfoPayload": {
|
|
87
|
+
"company": "FastEx",
|
|
88
|
+
"number": "123456789",
|
|
89
|
+
"url": "https://track.example.com/123456789"
|
|
90
|
+
}
|
|
91
|
+
}'
|
|
92
|
+
*/
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// https://shopify.dev/docs/api/admin-graphql/latest/queries/metafieldDefinition
|
|
3
3
|
|
|
4
4
|
const { credsValidator } = require('../validators');
|
|
5
|
-
const { ArgsWarden, objHasAny } = require('../utils');
|
|
5
|
+
const { ArgsWarden, actionSingleOrMultiple, everyIfArray, objHasAny } = require('../utils');
|
|
6
6
|
const { shopifyMetafieldDefinitionGet } = require('./shopifyMetafieldDefinitionGet');
|
|
7
7
|
const { shopifyMetafieldDefinitionCreate } = require('./shopifyMetafieldDefinitionCreate');
|
|
8
8
|
|
|
@@ -18,8 +18,8 @@ const metafieldDefinitionIdentifierValidator = (metafieldDefinitionIdentifier) =
|
|
|
18
18
|
|
|
19
19
|
const argsWarden = new ArgsWarden([
|
|
20
20
|
['fromStoreCredsPayload', credsValidator],
|
|
21
|
-
['toStoreCredsPayload', credsValidator],
|
|
22
|
-
['metafieldDefinitionIdentifier', metafieldDefinitionIdentifierValidator],
|
|
21
|
+
['toStoreCredsPayload', (i) => everyIfArray(credsValidator, i)],
|
|
22
|
+
['metafieldDefinitionIdentifier', (i) => everyIfArray(metafieldDefinitionIdentifierValidator, i)],
|
|
23
23
|
]);
|
|
24
24
|
|
|
25
25
|
const metafieldDefinitionInputFromDefinition = (sourceDefinition) => {
|
|
@@ -69,7 +69,7 @@ const metafieldDefinitionInputFromDefinition = (sourceDefinition) => {
|
|
|
69
69
|
return definitionInput;
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
-
const
|
|
72
|
+
const shopifyMetafieldDefinitionPropagateSingle = async (
|
|
73
73
|
fromStoreCredsPayload,
|
|
74
74
|
toStoreCredsPayload,
|
|
75
75
|
metafieldDefinitionIdentifier,
|
|
@@ -79,15 +79,6 @@ const shopifyMetafieldDefinitionPropagate = async (
|
|
|
79
79
|
} = {},
|
|
80
80
|
) => {
|
|
81
81
|
|
|
82
|
-
const rejectResponse = await argsWarden.responseIfRejectingArgs({
|
|
83
|
-
fromStoreCredsPayload,
|
|
84
|
-
toStoreCredsPayload,
|
|
85
|
-
metafieldDefinitionIdentifier,
|
|
86
|
-
});
|
|
87
|
-
if (rejectResponse) {
|
|
88
|
-
return rejectResponse;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
82
|
const sourceResponse = await shopifyMetafieldDefinitionGet(
|
|
92
83
|
fromStoreCredsPayload,
|
|
93
84
|
metafieldDefinitionIdentifier,
|
|
@@ -109,6 +100,29 @@ const shopifyMetafieldDefinitionPropagate = async (
|
|
|
109
100
|
};
|
|
110
101
|
}
|
|
111
102
|
|
|
103
|
+
const existingResponse = await shopifyMetafieldDefinitionGet(
|
|
104
|
+
toStoreCredsPayload,
|
|
105
|
+
metafieldDefinitionIdentifier,
|
|
106
|
+
{
|
|
107
|
+
apiVersion,
|
|
108
|
+
attrs: returnCreatedDefinitionAttrs,
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (!existingResponse.ok) {
|
|
113
|
+
return existingResponse;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (existingResponse.data) {
|
|
117
|
+
return {
|
|
118
|
+
ok: true,
|
|
119
|
+
data: existingResponse.data,
|
|
120
|
+
meta: {
|
|
121
|
+
alreadyExisted: true,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
112
126
|
const definitionInput = metafieldDefinitionInputFromDefinition(sourceResponse.data);
|
|
113
127
|
|
|
114
128
|
const {
|
|
@@ -143,6 +157,43 @@ const shopifyMetafieldDefinitionPropagate = async (
|
|
|
143
157
|
);
|
|
144
158
|
};
|
|
145
159
|
|
|
160
|
+
const shopifyMetafieldDefinitionPropagate = async (
|
|
161
|
+
fromStoreCredsPayload,
|
|
162
|
+
toStoreCredsPayload,
|
|
163
|
+
metafieldDefinitionIdentifier,
|
|
164
|
+
{
|
|
165
|
+
queueRunOptions,
|
|
166
|
+
apiVersion,
|
|
167
|
+
returnCreatedDefinitionAttrs = defaultReturnCreatedDefinitionAttrs,
|
|
168
|
+
} = {},
|
|
169
|
+
) => {
|
|
170
|
+
|
|
171
|
+
const rejectResponse = await argsWarden.responseIfRejectingArgs({
|
|
172
|
+
fromStoreCredsPayload,
|
|
173
|
+
toStoreCredsPayload,
|
|
174
|
+
metafieldDefinitionIdentifier,
|
|
175
|
+
});
|
|
176
|
+
if (rejectResponse) {
|
|
177
|
+
return rejectResponse;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return actionSingleOrMultiple(
|
|
181
|
+
[toStoreCredsPayload, metafieldDefinitionIdentifier],
|
|
182
|
+
shopifyMetafieldDefinitionPropagateSingle,
|
|
183
|
+
(toStoreCredsPayloadItem, metafieldDefinitionIdentifierItem) => ({
|
|
184
|
+
args: [
|
|
185
|
+
fromStoreCredsPayload,
|
|
186
|
+
toStoreCredsPayloadItem,
|
|
187
|
+
metafieldDefinitionIdentifierItem,
|
|
188
|
+
{ apiVersion, returnCreatedDefinitionAttrs },
|
|
189
|
+
],
|
|
190
|
+
}),
|
|
191
|
+
{
|
|
192
|
+
...(queueRunOptions ? { queueRunOptions } : {}),
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
};
|
|
196
|
+
|
|
146
197
|
const funcApiConfig = {
|
|
147
198
|
argsWarden,
|
|
148
199
|
};
|
|
@@ -157,11 +208,11 @@ curl -X POST "http://localhost:8000/shopifyMetafieldDefinitionPropagate" \
|
|
|
157
208
|
-H "Content-Type: application/json" \
|
|
158
209
|
-d '{
|
|
159
210
|
"fromStoreCredsPayload": { "credsPath": "shopify.au" },
|
|
160
|
-
"toStoreCredsPayload": { "credsPath": "shopify.us" },
|
|
211
|
+
"toStoreCredsPayload": [{ "credsPath": "shopify.us" }, { "credsPath": "shopify.uk" }],
|
|
161
212
|
"metafieldDefinitionIdentifier": {
|
|
162
213
|
"ownerType": "PRODUCT",
|
|
163
|
-
"namespace": "
|
|
164
|
-
"key": "
|
|
214
|
+
"namespace": "merch",
|
|
215
|
+
"key": "associations"
|
|
165
216
|
}
|
|
166
217
|
}'
|
|
167
218
|
*/
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
const { credsValidator } = require('../validators');
|
|
2
|
+
const { ArgsWarden, operationQueueRunner, logDeep, askQuestion } = require('../utils');
|
|
3
|
+
const { objHasAny } = require('../utils');
|
|
4
|
+
const { shopifyOrderGet } = require('../shopify/shopifyOrderGet');
|
|
5
|
+
const { shopifyFulfillmentCreate } = require('../shopify/shopifyFulfillmentCreate');
|
|
6
|
+
|
|
7
|
+
const orderIdentifierValidator = (orderIdentifier) => {
|
|
8
|
+
return objHasAny(orderIdentifier, [
|
|
9
|
+
'orderId',
|
|
10
|
+
'orderName',
|
|
11
|
+
]);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const fulfillmentPayloadValidator = (fulfillmentPayload) => {
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
fulfillAll,
|
|
18
|
+
itemsBySku,
|
|
19
|
+
// TODO: Support fulfilling items by line item id
|
|
20
|
+
|
|
21
|
+
} = fulfillmentPayload;
|
|
22
|
+
|
|
23
|
+
if (fulfillAll === true) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (itemsBySku) {
|
|
28
|
+
const quantities = Object.values(itemsBySku);
|
|
29
|
+
return quantities.length > 0 && quantities.every(quantity => typeof quantity === 'number');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return false;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const argsWarden = new ArgsWarden([
|
|
36
|
+
['credsPayload', credsValidator],
|
|
37
|
+
['orderIdentifier', orderIdentifierValidator],
|
|
38
|
+
['fulfillmentPayload', fulfillmentPayloadValidator],
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const shopifyOrderFulfill = async (
|
|
42
|
+
credsPayload,
|
|
43
|
+
orderIdentifier,
|
|
44
|
+
fulfillmentPayload,
|
|
45
|
+
{
|
|
46
|
+
apiVersion,
|
|
47
|
+
// TODO: Support return schema
|
|
48
|
+
// returnSchema = 'deletedThingId',
|
|
49
|
+
} = {},
|
|
50
|
+
) => {
|
|
51
|
+
|
|
52
|
+
const rejectResponse = await argsWarden.responseIfRejectingArgs({
|
|
53
|
+
credsPayload,
|
|
54
|
+
orderIdentifier,
|
|
55
|
+
fulfillmentPayload,
|
|
56
|
+
});
|
|
57
|
+
if (rejectResponse) {
|
|
58
|
+
return rejectResponse;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const {
|
|
62
|
+
fulfillAll = false,
|
|
63
|
+
itemsBySku,
|
|
64
|
+
notifyCustomer = false,
|
|
65
|
+
originAddress,
|
|
66
|
+
trackingInfo,
|
|
67
|
+
} = fulfillmentPayload;
|
|
68
|
+
|
|
69
|
+
const fetchFulfillmentOrdersLimit = 10;
|
|
70
|
+
const fetchFulfillmentsLimit = 10;
|
|
71
|
+
const fetchTrackingInfoLimit = 10;
|
|
72
|
+
const fetchLineItemsLimit = 250;
|
|
73
|
+
|
|
74
|
+
const openFulfillmentOrdersQuery = `displayable:true AND (request_status:UNSUBMITTED OR request_status:ACCEPTED)`;
|
|
75
|
+
|
|
76
|
+
const ORDER_ATTRS = `
|
|
77
|
+
fulfillable
|
|
78
|
+
fulfillments(first: ${ fetchFulfillmentsLimit }) {
|
|
79
|
+
trackingInfo(first: ${ fetchTrackingInfoLimit }) {
|
|
80
|
+
number
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
fulfillmentOrders(first: ${ fetchFulfillmentOrdersLimit }, query: "${ openFulfillmentOrdersQuery }") {
|
|
84
|
+
edges {
|
|
85
|
+
node {
|
|
86
|
+
id
|
|
87
|
+
requestStatus
|
|
88
|
+
${ itemsBySku ? `
|
|
89
|
+
lineItems (first: ${ fetchLineItemsLimit }) {
|
|
90
|
+
edges {
|
|
91
|
+
node {
|
|
92
|
+
id
|
|
93
|
+
sku
|
|
94
|
+
remainingQuantity
|
|
95
|
+
requiresShipping
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
` : '' }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
// Get open fulfillment orders
|
|
106
|
+
const orderResponse = await shopifyOrderGet(
|
|
107
|
+
credsPayload,
|
|
108
|
+
orderIdentifier,
|
|
109
|
+
{
|
|
110
|
+
apiVersion,
|
|
111
|
+
attrs: ORDER_ATTRS,
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const { ok: orderOk, data: order } = orderResponse;
|
|
116
|
+
if (!orderOk || !order) {
|
|
117
|
+
return orderResponse;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const {
|
|
121
|
+
fulfillable,
|
|
122
|
+
fulfillments,
|
|
123
|
+
fulfillmentOrders,
|
|
124
|
+
} = order;
|
|
125
|
+
|
|
126
|
+
if (fulfillments.length >= fetchFulfillmentsLimit) {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
error: {
|
|
130
|
+
code: 'LIMIT_REACHED',
|
|
131
|
+
message: `We retrieved ${ fetchFulfillmentsLimit } fulfillments, so there may be more. Please adjust the function.`,
|
|
132
|
+
},
|
|
133
|
+
data: order,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (fulfillments.some(f => f?.trackingInfo?.length > fetchTrackingInfoLimit)) {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
error: {
|
|
141
|
+
code: 'LIMIT_REACHED',
|
|
142
|
+
message: `We retrieved ${ fetchTrackingInfoLimit } tracking info, so there may be more. Please adjust the function.`,
|
|
143
|
+
},
|
|
144
|
+
data: order,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (fulfillmentOrders.length >= fetchFulfillmentOrdersLimit) {
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
error: {
|
|
152
|
+
code: 'LIMIT_REACHED',
|
|
153
|
+
message: `We retrieved ${ fetchFulfillmentOrdersLimit } open fulfillment orders, so there may be more. Please adjust the function.`,
|
|
154
|
+
},
|
|
155
|
+
data: order,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!fulfillable) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
error: 'Order is not fulfillable',
|
|
163
|
+
data: order,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const seenTrackingNumbers = fulfillments.map(f => f?.trackingInfo?.map(t => t.number)).flat().filter(Boolean);
|
|
168
|
+
|
|
169
|
+
if (trackingInfo?.number && seenTrackingNumbers.includes(trackingInfo.number)) {
|
|
170
|
+
return {
|
|
171
|
+
ok: true,
|
|
172
|
+
meta: {
|
|
173
|
+
alreadyFulfilled: true,
|
|
174
|
+
trackingNumber: trackingInfo.number,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// if using fulfillAll, fulfill them
|
|
180
|
+
if (fulfillAll === true) {
|
|
181
|
+
return shopifyFulfillmentCreate(
|
|
182
|
+
credsPayload,
|
|
183
|
+
fulfillmentOrders.map(fulfillmentOrder => {
|
|
184
|
+
const {
|
|
185
|
+
id: fulfillmentOrderGid,
|
|
186
|
+
} = fulfillmentOrder;
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
lineItemsByFulfillmentOrder: [{
|
|
190
|
+
fulfillmentOrderId: fulfillmentOrderGid,
|
|
191
|
+
}],
|
|
192
|
+
notifyCustomer,
|
|
193
|
+
originAddress,
|
|
194
|
+
trackingInfo,
|
|
195
|
+
};
|
|
196
|
+
}),
|
|
197
|
+
{
|
|
198
|
+
apiVersion,
|
|
199
|
+
},
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// if using itemsBySku, iterate over unfulfilled line items and decrement until complete, making a queue of fulfillments to action
|
|
204
|
+
if (itemsBySku) {
|
|
205
|
+
const depletableItemsBySku = { ...itemsBySku };
|
|
206
|
+
const shopifyFulfillmentCreatePayloads = [];
|
|
207
|
+
|
|
208
|
+
for (const fulfillmentOrder of fulfillmentOrders) {
|
|
209
|
+
const {
|
|
210
|
+
id: fulfillmentOrderGid,
|
|
211
|
+
lineItems = [],
|
|
212
|
+
} = fulfillmentOrder;
|
|
213
|
+
|
|
214
|
+
if (lineItems.length >= fetchLineItemsLimit) {
|
|
215
|
+
return {
|
|
216
|
+
ok: false,
|
|
217
|
+
error: {
|
|
218
|
+
code: 'LIMIT_REACHED',
|
|
219
|
+
message: `We retrieved ${ fetchLineItemsLimit } line items on a fulfillment order, so there may be more. Please adjust the function.`,
|
|
220
|
+
},
|
|
221
|
+
data: order,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const fulfillmentOrderLineItems = [];
|
|
226
|
+
|
|
227
|
+
for (const lineItem of lineItems) {
|
|
228
|
+
const {
|
|
229
|
+
id: lineItemGid,
|
|
230
|
+
sku,
|
|
231
|
+
remainingQuantity,
|
|
232
|
+
requiresShipping,
|
|
233
|
+
} = lineItem;
|
|
234
|
+
|
|
235
|
+
if (!requiresShipping || remainingQuantity <= 0) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const depletableQuantity = depletableItemsBySku[sku];
|
|
240
|
+
if (!depletableQuantity) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const quantity = Math.min(remainingQuantity, depletableQuantity);
|
|
245
|
+
fulfillmentOrderLineItems.push({
|
|
246
|
+
id: lineItemGid,
|
|
247
|
+
quantity,
|
|
248
|
+
});
|
|
249
|
+
depletableItemsBySku[sku] -= quantity;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (fulfillmentOrderLineItems.length > 0) {
|
|
253
|
+
shopifyFulfillmentCreatePayloads.push([
|
|
254
|
+
credsPayload,
|
|
255
|
+
{
|
|
256
|
+
lineItemsByFulfillmentOrder: [{
|
|
257
|
+
fulfillmentOrderId: fulfillmentOrderGid,
|
|
258
|
+
fulfillmentOrderLineItems,
|
|
259
|
+
}],
|
|
260
|
+
notifyCustomer,
|
|
261
|
+
originAddress,
|
|
262
|
+
trackingInfo,
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
apiVersion,
|
|
266
|
+
},
|
|
267
|
+
]);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (shopifyFulfillmentCreatePayloads.length === 0) {
|
|
272
|
+
return {
|
|
273
|
+
ok: false,
|
|
274
|
+
error: {
|
|
275
|
+
message: 'No fulfillable line items found',
|
|
276
|
+
},
|
|
277
|
+
data: order,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
logDeep({ depletableItemsBySku, shopifyFulfillmentCreatePayloads });
|
|
282
|
+
await askQuestion('Continue?');
|
|
283
|
+
|
|
284
|
+
return operationQueueRunner(
|
|
285
|
+
shopifyFulfillmentCreate,
|
|
286
|
+
shopifyFulfillmentCreatePayloads,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
ok: false,
|
|
292
|
+
error: `I guess we don't do that yet`,
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const funcApiConfig = {
|
|
297
|
+
argsWarden,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
module.exports = {
|
|
301
|
+
shopifyOrderFulfill,
|
|
302
|
+
funcApiConfig,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/*
|
|
306
|
+
curl -X POST "http://localhost:8000/shopifyOrderFulfill" \
|
|
307
|
+
-H "Content-Type: application/json" \
|
|
308
|
+
-d `{
|
|
309
|
+
"credsPayload": { "credsPath": "shopify.au" },
|
|
310
|
+
"orderIdentifier": { "orderId": "104188477512" },
|
|
311
|
+
"fulfillmentPayload": {
|
|
312
|
+
"fulfillAll": true,
|
|
313
|
+
"notifyCustomer": true,
|
|
314
|
+
"originAddress": { "countryCode": "AU" },
|
|
315
|
+
"trackingInfo": {
|
|
316
|
+
"number": "1234567890",
|
|
317
|
+
"company": "Kiki's Delivery Service",
|
|
318
|
+
"url": "https://www.studioghibli.com.au/kikisdeliveryservice"
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}`
|
|
322
|
+
*/
|
|
@@ -8,9 +8,19 @@ const {
|
|
|
8
8
|
const interpretStarshipitResponse = async (state) => {
|
|
9
9
|
const { response } = state;
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const responseData = response.data || {};
|
|
12
|
+
const {
|
|
13
|
+
success,
|
|
14
|
+
succeeded,
|
|
15
|
+
results,
|
|
16
|
+
errors,
|
|
17
|
+
data: nestedData,
|
|
18
|
+
...rest
|
|
19
|
+
} = responseData;
|
|
20
|
+
|
|
21
|
+
const ok = success === true || succeeded === true;
|
|
12
22
|
|
|
13
|
-
if (!
|
|
23
|
+
if (!ok) {
|
|
14
24
|
const firstError = errors?.[0];
|
|
15
25
|
|
|
16
26
|
// TODO: Consider removing data if errors
|
|
@@ -27,10 +37,25 @@ const interpretStarshipitResponse = async (state) => {
|
|
|
27
37
|
};
|
|
28
38
|
}
|
|
29
39
|
|
|
40
|
+
let data;
|
|
41
|
+
if (results !== undefined) {
|
|
42
|
+
data = results;
|
|
43
|
+
} else if (nestedData && typeof nestedData === 'object' && !Array.isArray(nestedData)) {
|
|
44
|
+
// List endpoints return { page_*, data: { orders|products }, succeeded }
|
|
45
|
+
data = {
|
|
46
|
+
...rest,
|
|
47
|
+
...nestedData,
|
|
48
|
+
...success !== undefined && { success },
|
|
49
|
+
...succeeded !== undefined && { succeeded },
|
|
50
|
+
};
|
|
51
|
+
} else {
|
|
52
|
+
data = responseData;
|
|
53
|
+
}
|
|
54
|
+
|
|
30
55
|
return {
|
|
31
56
|
response: {
|
|
32
57
|
...response,
|
|
33
|
-
data
|
|
58
|
+
data,
|
|
34
59
|
},
|
|
35
60
|
};
|
|
36
61
|
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// https://api-docs.starshipit.com/#0aef707f-e2f5-493a-a382-8235c00c9c18
|
|
2
|
+
|
|
3
|
+
const { ArgsWarden } = require('../utils');
|
|
4
|
+
const { credsValidator } = require('../validators');
|
|
5
|
+
const { starshipitGet, starshipitGetter } = require('../starshipit/starshipitGet');
|
|
6
|
+
|
|
7
|
+
const ORDERS_MAX_PER_PAGE = 500;
|
|
8
|
+
|
|
9
|
+
const argsWarden = new ArgsWarden([
|
|
10
|
+
['credsPayload', credsValidator],
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const starshipitOrdersGet = async (
|
|
14
|
+
returnGetter,
|
|
15
|
+
|
|
16
|
+
credsPayload,
|
|
17
|
+
{
|
|
18
|
+
orderId,
|
|
19
|
+
orderNumber,
|
|
20
|
+
status,
|
|
21
|
+
filter,
|
|
22
|
+
include,
|
|
23
|
+
sortColumn,
|
|
24
|
+
sortDirection,
|
|
25
|
+
perPage = ORDERS_MAX_PER_PAGE,
|
|
26
|
+
...getterOptions
|
|
27
|
+
} = {},
|
|
28
|
+
) => {
|
|
29
|
+
|
|
30
|
+
const rejectResponse = await argsWarden.responseIfRejectingArgs({
|
|
31
|
+
credsPayload,
|
|
32
|
+
});
|
|
33
|
+
if (rejectResponse) {
|
|
34
|
+
return rejectResponse;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const getterArgs = [
|
|
38
|
+
credsPayload,
|
|
39
|
+
'/orders',
|
|
40
|
+
{
|
|
41
|
+
nodeName: 'orders',
|
|
42
|
+
perPage,
|
|
43
|
+
params: {
|
|
44
|
+
...orderId && { order_id: orderId },
|
|
45
|
+
...orderNumber && { order_number: orderNumber },
|
|
46
|
+
...status && { status },
|
|
47
|
+
...filter && { filter },
|
|
48
|
+
...include && { include },
|
|
49
|
+
...sortColumn && { sort_column: sortColumn },
|
|
50
|
+
...sortDirection && { sort_direction: sortDirection },
|
|
51
|
+
},
|
|
52
|
+
digester: (response) => {
|
|
53
|
+
if (!response?.ok) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
const { orders, order } = response.data || {};
|
|
57
|
+
if (Array.isArray(orders)) {
|
|
58
|
+
return orders;
|
|
59
|
+
}
|
|
60
|
+
if (order) {
|
|
61
|
+
return [order];
|
|
62
|
+
}
|
|
63
|
+
return [];
|
|
64
|
+
},
|
|
65
|
+
...getterOptions,
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
return returnGetter
|
|
70
|
+
? starshipitGetter(...getterArgs)
|
|
71
|
+
: starshipitGet(...getterArgs);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const funcApiConfig = {
|
|
75
|
+
argsWarden,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
starshipitOrdersGet: (...args) => starshipitOrdersGet(false, ...args),
|
|
80
|
+
starshipitOrdersGetter: (...args) => starshipitOrdersGet(true, ...args),
|
|
81
|
+
funcApiConfig,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/*
|
|
85
|
+
curl -X POST "http://localhost:8000/starshipitOrdersGet" \
|
|
86
|
+
-H "Content-Type: application/json" \
|
|
87
|
+
-d '{
|
|
88
|
+
"credsPayload": { "credsPath": "starshipit.wf" },
|
|
89
|
+
"options": {
|
|
90
|
+
"status": "Shipped",
|
|
91
|
+
"include": ["Items", "Packages"],
|
|
92
|
+
"perPage": 2,
|
|
93
|
+
"limit": 2
|
|
94
|
+
}
|
|
95
|
+
}'
|
|
96
|
+
*/
|
package/api/utils.js
CHANGED
|
@@ -17,6 +17,14 @@ const objHasAny = (obj, keys) => {
|
|
|
17
17
|
return keys.some((key) => obj[key] !== undefined);
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
const objHasAll = (obj, keys) => {
|
|
21
|
+
if (obj == null || typeof obj !== 'object') {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return keys.every((key) => obj[key] !== undefined);
|
|
26
|
+
};
|
|
27
|
+
|
|
20
28
|
const capitaliseString = (string) => `${ string[0].toUpperCase() }${ string.slice(1) }`;
|
|
21
29
|
const sentenceCaseString = (string) => `${ string[0].toLowerCase() }${ string.slice(1) }`;
|
|
22
30
|
|
|
@@ -199,7 +207,19 @@ const customFetch = async (url, {
|
|
|
199
207
|
}
|
|
200
208
|
|
|
201
209
|
if (params) {
|
|
202
|
-
const search = new URLSearchParams(
|
|
210
|
+
const search = new URLSearchParams();
|
|
211
|
+
for (const [key, value] of Object.entries(params)) {
|
|
212
|
+
if (value === undefined || value === null) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (Array.isArray(value)) {
|
|
216
|
+
for (const entry of value) {
|
|
217
|
+
search.append(key, entry);
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
search.append(key, value);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
203
223
|
url += (url.includes('?') ? '&' : '?') + search.toString();
|
|
204
224
|
}
|
|
205
225
|
|
|
@@ -790,6 +810,7 @@ class OperationQueue {
|
|
|
790
810
|
async run({
|
|
791
811
|
interval = false,
|
|
792
812
|
verbose = true,
|
|
813
|
+
inspect = false,
|
|
793
814
|
} = {}) {
|
|
794
815
|
|
|
795
816
|
// Run at interval
|
|
@@ -825,7 +846,18 @@ class OperationQueue {
|
|
|
825
846
|
// Run in sequence
|
|
826
847
|
const results = [];
|
|
827
848
|
for (const op of this.queue) {
|
|
849
|
+
|
|
828
850
|
const result = await op.run();
|
|
851
|
+
|
|
852
|
+
if (inspect !== false && (
|
|
853
|
+
inspect === true
|
|
854
|
+
|| (typeof inspect === 'function' ? inspect(result) : objectMatchesPartial(result, inspect))
|
|
855
|
+
)) {
|
|
856
|
+
inspect && logDeep({ op });
|
|
857
|
+
inspect && logDeep({ result });
|
|
858
|
+
inspect && await askQuestion('Continue?');
|
|
859
|
+
}
|
|
860
|
+
|
|
829
861
|
results.push(result);
|
|
830
862
|
if (verbose) {
|
|
831
863
|
console.log(`${ results.length } / ${ this.queue.length }`);
|
|
@@ -837,6 +869,16 @@ class OperationQueue {
|
|
|
837
869
|
}
|
|
838
870
|
}
|
|
839
871
|
|
|
872
|
+
const operationQueueRunner = async (func, payloads, { queueRunOptions = {} } = {}) => {
|
|
873
|
+
const queue = new OperationQueue(payloads.map(payload => new Operation(
|
|
874
|
+
func,
|
|
875
|
+
{ args: payload },
|
|
876
|
+
)));
|
|
877
|
+
|
|
878
|
+
const queueResponses = await queue.run(queueRunOptions);
|
|
879
|
+
return responseArrayToResponse(queueResponses);
|
|
880
|
+
};
|
|
881
|
+
|
|
840
882
|
const simpleSort = (arr, prop, { reverse } = {}) => {
|
|
841
883
|
return [...arr].sort((a, b) =>
|
|
842
884
|
reverse ? b[prop] - a[prop] : a[prop] - b[prop],
|
|
@@ -1236,10 +1278,48 @@ const gidToId = (gid) => {
|
|
|
1236
1278
|
return gid.split('/').pop();
|
|
1237
1279
|
};
|
|
1238
1280
|
|
|
1281
|
+
const objectToArray = (object, { keyProp } = {}) => {
|
|
1282
|
+
if (!keyProp) {
|
|
1283
|
+
return Object.values(object);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
return Object.entries(object).map(([key, value]) => ({
|
|
1287
|
+
[keyProp]: key,
|
|
1288
|
+
...value,
|
|
1289
|
+
}));
|
|
1290
|
+
};
|
|
1291
|
+
|
|
1292
|
+
const objectMatchesPartial = (object, partial) => {
|
|
1293
|
+
if (object === partial) {
|
|
1294
|
+
return true;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
if (
|
|
1298
|
+
partial === null
|
|
1299
|
+
|| typeof partial !== 'object'
|
|
1300
|
+
|| object === null
|
|
1301
|
+
|| typeof object !== 'object'
|
|
1302
|
+
) {
|
|
1303
|
+
return false;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
if (Array.isArray(partial)) {
|
|
1307
|
+
if (!Array.isArray(object)) {
|
|
1308
|
+
return false;
|
|
1309
|
+
}
|
|
1310
|
+
return partial.every((item, i) => objectMatchesPartial(object[i], item));
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
return Object.entries(partial).every(([key, value]) => (
|
|
1314
|
+
objectMatchesPartial(object[key], value)
|
|
1315
|
+
));
|
|
1316
|
+
};
|
|
1317
|
+
|
|
1239
1318
|
module.exports = {
|
|
1240
1319
|
wait,
|
|
1241
1320
|
timeMs,
|
|
1242
1321
|
objHasAny,
|
|
1322
|
+
objHasAll,
|
|
1243
1323
|
capitaliseString,
|
|
1244
1324
|
normalise,
|
|
1245
1325
|
credsFromPayload,
|
|
@@ -1264,6 +1344,7 @@ module.exports = {
|
|
|
1264
1344
|
responseResultsByOutcome,
|
|
1265
1345
|
Operation,
|
|
1266
1346
|
OperationQueue,
|
|
1347
|
+
operationQueueRunner,
|
|
1267
1348
|
actionSingleOrMultiple,
|
|
1268
1349
|
Processor,
|
|
1269
1350
|
Getter,
|
|
@@ -1271,4 +1352,6 @@ module.exports = {
|
|
|
1271
1352
|
ArgsWarden,
|
|
1272
1353
|
gidToId,
|
|
1273
1354
|
valueProvided,
|
|
1355
|
+
objectToArray,
|
|
1356
|
+
objectMatchesPartial,
|
|
1274
1357
|
};
|